diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/404.html b/404.html new file mode 100644 index 0000000..44ce0dd --- /dev/null +++ b/404.html @@ -0,0 +1,144 @@ + + + + + + + + 404 Page not found + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

¯\_(ツ)_/¯

+

Nothing here

+
+
+ + + + + + + + + + diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..950ec74 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +jauntsdn.com \ No newline at end of file diff --git a/categories/index.html b/categories/index.html new file mode 100644 index 0000000..9229d76 --- /dev/null +++ b/categories/index.html @@ -0,0 +1,150 @@ + + + + + + + + Categories + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

Categories

+ + + +
+ + + + + + + + + + + + diff --git a/categories/index.xml b/categories/index.xml new file mode 100644 index 0000000..37bb567 --- /dev/null +++ b/categories/index.xml @@ -0,0 +1,14 @@ + + + + Categories on JAUNT.SDN + https://jauntsdn.com/categories/ + Recent content in Categories on JAUNT.SDN + Hugo -- gohugo.io + en-us + + + + + + \ No newline at end of file diff --git a/categories/page/1/index.html b/categories/page/1/index.html new file mode 100644 index 0000000..8f5b117 --- /dev/null +++ b/categories/page/1/index.html @@ -0,0 +1 @@ +https://jauntsdn.com/categories/ \ No newline at end of file diff --git a/css/desktop.css b/css/desktop.css new file mode 100644 index 0000000..782f685 --- /dev/null +++ b/css/desktop.css @@ -0,0 +1,31 @@ +.intro { + transform: translateY(22vh); +} + +.intro > h1 { + color: #212121; + font-size: 80px; +} + +.intro > h2 { + color: #757575; + font-size: 21px; +} + +.mstreams { + width: 510px; + transform: translateY(20vh); +} + +.mstreams-capt { + font-size: 25px; +} + +.mstreams-desc { + font-size: 19px; + padding-left: 24px; +} + +.navbar-nav > li > a { + font-size: 16px; +} \ No newline at end of file diff --git a/css/handheld-h-narrow.css b/css/handheld-h-narrow.css new file mode 100644 index 0000000..3daaf3c --- /dev/null +++ b/css/handheld-h-narrow.css @@ -0,0 +1,27 @@ +.intro > h1 { + color: #212121; + font-size: 56px; + margin-bottom: 0px; +} + +.intro > h2 { + color: #757575; + font-size: 15px; +} + +div.intro h1 + h2 { + margin-top: 0px; +} + +.mstreams { + width: 510px; + transform: translateY(5vh); +} + +.mstreams-capt { + font-size: 18px; +} + +.mstreams-desc { + font-size: 16px; +} \ No newline at end of file diff --git a/css/handheld-h.css b/css/handheld-h.css new file mode 100644 index 0000000..a0371e8 --- /dev/null +++ b/css/handheld-h.css @@ -0,0 +1,32 @@ +.intro { + transform: translateY(-3vh); +} + +.intro > h1 { + color: #212121; + font-size: 60px; + margin-bottom: 0px; +} + +.intro > h2 { + color: #757575; + font-size: 16px; +} + +div.intro h1 + h2 { + margin-top: 0px; +} + +.mstreams { + width: 510px; + transform: translateY(9vh); +} + +.mstreams-capt { + font-size: 18px; +} + +.mstreams-desc { + font-size: 17px; + padding-left: 18px; +} \ No newline at end of file diff --git a/css/handheld-v-narrow.css b/css/handheld-v-narrow.css new file mode 100644 index 0000000..7e1b65d --- /dev/null +++ b/css/handheld-v-narrow.css @@ -0,0 +1,23 @@ +.intro > h1 { + color: #212121; + font-size: 57px; +} + +.intro > h2 { + color: #757575; + font-size: 15px; +} + +.mstreams { + width: auto; + transform: translateY(15vh); +} + +.mstreams-capt { + font-size: 19px; +} + +.mstreams-desc { + font-size: 17px; + padding-left: 18px; +} \ No newline at end of file diff --git a/css/handheld-v.css b/css/handheld-v.css new file mode 100644 index 0000000..383bfe5 --- /dev/null +++ b/css/handheld-v.css @@ -0,0 +1,32 @@ +.intro { + transform: translateY(15vh); +} + +.intro > h1 { + color: #212121; + font-size: 60px; +} + +.intro > h2 { + color: #757575; + font-size: 16px; +} + +div.intro h1 + h2 { + margin-top: 5px; +} + +.mstreams { + width: auto; + transform: translateY(15vh); +} + +.mstreams-capt { + font-size: 19px; +} + +.mstreams-desc { + font-size: 17px; + padding-left: 18px; +} + diff --git a/css/laptop.css b/css/laptop.css new file mode 100644 index 0000000..19870e6 --- /dev/null +++ b/css/laptop.css @@ -0,0 +1,23 @@ +.intro > h1 { + color: #212121; + font-size: 77px; +} + +.intro > h2 { + color: #757575; + font-size: 20px; +} + +.mstreams { + width: 510px; + transform: translateY(15vh); +} + +.mstreams-capt { + font-size: 20px; +} + +.mstreams-desc { + font-size: 19px; + padding-left: 18px; +} \ No newline at end of file diff --git a/css/main.css b/css/main.css new file mode 100644 index 0000000..a4548eb --- /dev/null +++ b/css/main.css @@ -0,0 +1,171 @@ +html, body { + height: 100%; +} + +body { + padding-top: 55px; + display: flex; + text-align: center; + flex-direction: column; +} + +html body { + line-height: 2.1rem; + font-size: 15px; +} + +p > code { + color: #000000; + background-color: #e9e9e9; +} + +li > code { + color: #000000; + background-color: #e9e9e9; +} + +main { + margin: auto; + padding: 25px; + flex: 1 0 auto; + max-width: 750px; +} + +img { + max-width: 100%; +} + +/* Page content */ + +.content { + padding-top: 20px; +} + +/* Profile picture */ + +.profile { + width: 10vh; + height: 10vh; + border-radius: 50%; +} + +/* Colored links */ + +a:link, a:visited { + color: var(--accent); +} + +a.icon:hover { + text-decoration: none; +} + +a:hover { + color: var(--accent) !important; +} + +/* Copyright message */ + +.copyright { + margin: 15px 0; +} + +/* Paginator */ + +.pages { + padding: 15px 0; +} + +.pages-icon { + padding: 0 15px; +} + +/* List item for posts/projects */ + +.item { + padding: 10px 0; +} + +.item-tag { + background-color: var(--accent); + border-radius: 2px; + padding: 2px 4px; + font-family: "Raleway"; + font-size: 95%; + font-weight: bold; +} + +div.content code { + font-family: "IBM Plex Mono"; +} + +div.highlight code { + font-family: "IBM Plex Mono"; +} + +/* div.content > pre > code { + font-family: "IBM Plex Mono"; +} + +div.content > ul > li > p > code { + font-family: "IBM Plex Mono"; +} + +div.content > p > code { + font-family: "IBM Plex Mono"; +} + +div.highlight > pre > code { + font-family: "IBM Plex Mono"; +} */ + +/* Navigation icons */ + +.navbar-icon { + font-size: 125%; + display: inline-block !important; +} + +button.navbar-toggle { + border: 0px solid transparent; +} + +/* Colored borders at top/bottom of page */ + +.navbar.navbar-default { + border-top: var(--border-width) solid var(--accent); +} + + +div h2 { + margin-top: 25px; +} + +nav.navbar-default { + background-color: #ffffff; + font-family: Raleway; +} + +footer { + border-bottom: solid white; + font-family: Raleway; +} + +div.intro h1 { + font-family: Raleway; + font-weight: 300; + margin-bottom: -8px; +} + +div.intro h2 { + margin-top: -5px; + font-family: Raleway; + font-weight: 300; +} + +main h2 { + font-family: Raleway; +} + +div.item { + font-family: Raleway; +} \ No newline at end of file diff --git a/css/overflow.css b/css/overflow.css new file mode 100644 index 0000000..e9118eb --- /dev/null +++ b/css/overflow.css @@ -0,0 +1,3 @@ +html, body { + overflow-x:hidden; +} \ No newline at end of file diff --git a/favicon.png b/favicon.png new file mode 100644 index 0000000..2163951 Binary files /dev/null and b/favicon.png differ diff --git a/img/1000k-alloc-part2.png b/img/1000k-alloc-part2.png new file mode 100644 index 0000000..a8ac3d4 Binary files /dev/null and b/img/1000k-alloc-part2.png differ diff --git a/img/1000k-cpu-part2.png b/img/1000k-cpu-part2.png new file mode 100644 index 0000000..e927a7d Binary files /dev/null and b/img/1000k-cpu-part2.png differ diff --git a/img/1000k-streams-part2.png b/img/1000k-streams-part2.png new file mode 100644 index 0000000..dff644c Binary files /dev/null and b/img/1000k-streams-part2.png differ diff --git a/img/250k-cpu.png b/img/250k-cpu.png new file mode 100644 index 0000000..9380411 Binary files /dev/null and b/img/250k-cpu.png differ diff --git a/img/250k-memory.png b/img/250k-memory.png new file mode 100644 index 0000000..1fde39b Binary files /dev/null and b/img/250k-memory.png differ diff --git a/img/250k-streams.png b/img/250k-streams.png new file mode 100644 index 0000000..6b0e9fd Binary files /dev/null and b/img/250k-streams.png differ diff --git a/img/500k-cpu-part2.png b/img/500k-cpu-part2.png new file mode 100644 index 0000000..f5a2f57 Binary files /dev/null and b/img/500k-cpu-part2.png differ diff --git a/img/500k-cpu.png b/img/500k-cpu.png new file mode 100644 index 0000000..f3977cc Binary files /dev/null and b/img/500k-cpu.png differ diff --git a/img/500k-memory.png b/img/500k-memory.png new file mode 100644 index 0000000..429272c Binary files /dev/null and b/img/500k-memory.png differ diff --git a/img/500k-streams-part2.png b/img/500k-streams-part2.png new file mode 100644 index 0000000..00bb383 Binary files /dev/null and b/img/500k-streams-part2.png differ diff --git a/img/500k-streams.png b/img/500k-streams.png new file mode 100644 index 0000000..9d64c15 Binary files /dev/null and b/img/500k-streams.png differ diff --git a/img/750k-alloc.png b/img/750k-alloc.png new file mode 100644 index 0000000..ac8e6fd Binary files /dev/null and b/img/750k-alloc.png differ diff --git a/img/750k-cpu.png b/img/750k-cpu.png new file mode 100644 index 0000000..5eb5aa6 Binary files /dev/null and b/img/750k-cpu.png differ diff --git a/img/750k-gc.png b/img/750k-gc.png new file mode 100644 index 0000000..0b6f99f Binary files /dev/null and b/img/750k-gc.png differ diff --git a/img/750k-memory.png b/img/750k-memory.png new file mode 100644 index 0000000..ba5bb03 Binary files /dev/null and b/img/750k-memory.png differ diff --git a/img/750k-streams.png b/img/750k-streams.png new file mode 100644 index 0000000..57900a6 Binary files /dev/null and b/img/750k-streams.png differ diff --git a/img/encrypted-ws-http2.webp b/img/encrypted-ws-http2.webp new file mode 100644 index 0000000..e6573c0 Binary files /dev/null and b/img/encrypted-ws-http2.webp differ diff --git a/img/encrypted.webp b/img/encrypted.webp new file mode 100644 index 0000000..3982152 Binary files /dev/null and b/img/encrypted.webp differ diff --git a/img/favicon.png b/img/favicon.png new file mode 100644 index 0000000..2163951 Binary files /dev/null and b/img/favicon.png differ diff --git a/img/jaunt_icon.png b/img/jaunt_icon.png new file mode 100644 index 0000000..42d7c75 Binary files /dev/null and b/img/jaunt_icon.png differ diff --git a/img/jaunt_icon_mobile.png b/img/jaunt_icon_mobile.png new file mode 100644 index 0000000..87c86db Binary files /dev/null and b/img/jaunt_icon_mobile.png differ diff --git a/img/jaunt_icon_small.png b/img/jaunt_icon_small.png new file mode 100644 index 0000000..64c91a3 Binary files /dev/null and b/img/jaunt_icon_small.png differ diff --git a/img/mstreams-logo.svg b/img/mstreams-logo.svg new file mode 100644 index 0000000..888c11c --- /dev/null +++ b/img/mstreams-logo.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/img/mstreams.png b/img/mstreams.png new file mode 100644 index 0000000..a5a6b4f Binary files /dev/null and b/img/mstreams.png differ diff --git a/img/non-encrypted.webp b/img/non-encrypted.webp new file mode 100644 index 0000000..bccc9ab Binary files /dev/null and b/img/non-encrypted.webp differ diff --git a/img/trisocket.png b/img/trisocket.png new file mode 100644 index 0000000..3f3c894 Binary files /dev/null and b/img/trisocket.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..edc4e91 --- /dev/null +++ b/index.html @@ -0,0 +1,154 @@ + + + + + + + + JAUNT.SDN + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +

JAUNT.SDN

+

SOFTWARE AND NETWORK APPLICATIONS

+ +
+ +
+ Message-Streams is very fast GRPC-like & GRPC-compatible services on JVM with rich streaming models over multiple network transports.
+
+
+
+ + + + + + + + + + diff --git a/index.xml b/index.xml new file mode 100644 index 0000000..82e0fed --- /dev/null +++ b/index.xml @@ -0,0 +1,135 @@ + + + + JAUNT.SDN + https://jauntsdn.com/ + Recent content on JAUNT.SDN + Hugo -- gohugo.io + en-us + Wed, 01 Feb 2023 10:18:00 +0200 + + + + + + 📌 Summary: alternative RSocket library for high performance network applications on JVM + https://jauntsdn.com/post/rsocket-summary/ + Wed, 01 Feb 2023 10:18:00 +0200 + + https://jauntsdn.com/post/rsocket-summary/ + Summary lean & very fast GRPC-like services on JVM with rich streaming models over multiple data-center and internet transports. multiple APIs: CompletableFuture & virtual threads; traditional streaming GRPC-stub (StreamObserver), or reactive with smallrye-mutiny, rxjava, reactor pluggable networking: tcp, unix sockets, VM sockets; grpc, websockets, websockets-over-http2 service APIs / codegen stubs (Message-Streams) are split from library runtime (RSocket-JVM, including network transports, load estimators, load balancers, metrics) transparent origin (RPC) & proxy load estimation for low-cost yet efficient load balancers native image support with graalvm Project consists of API modules for each vendor library - Message-Streams, and respective runtime implementations - RSocket-JVM. + + + + Jaunt-RSocket-RPC, Spring-RSocket, GRPC: quantitative and qualitative comparison + https://jauntsdn.com/post/rsocket-vs-spring/ + Fri, 03 Sep 2021 10:18:00 +0200 + + https://jauntsdn.com/post/rsocket-vs-spring/ + Earlier this year we described weaknesses of Spring RSocket-java implementation that resulted in awful throughput, latency & garbage production: key metrics for core networking library. +Latest Spring-RSocket uses RSocket/RSocket-java version 1.1.1, and wraps up almost a year of progress made on the library. +The goal of this post is to re-evaluate that progress against both current jauntsdn/RSocket-RPC (not yet publicly available), and grpc/grpc-java. +Jauntsdn-RSocket-RPC vs Spring-RSocket-CBOR: quantitative comparison jauntsdn/RSocket-RPC is remote procedure call system using streaming libraries (GRPC-API or several reactive - including RSocket-reactor) for networking & APIs, and Protocol Buffers as sole data format. + + + + Alternative RSocket-RPC: fast application services communication and transparent GRPC bridge + https://jauntsdn.com/post/rsocket-grpc/ + Thu, 20 May 2021 08:00:00 +0200 + + https://jauntsdn.com/post/rsocket-grpc/ + Exchanging millions of buffers per second with plain RSockets is not useful for business applications because they need structured data instead of raw bytes. +That’s why there was RSocket-RPC - remote procedure call system on top of Protocol Buffers with language agnostic service definitions. It uses code-generation for performance equivalent to hand-written code. +Protocol buffers have tiny on-wire overhead, acceptable performance and is native format of Grpc - important for interoperability and available tooling that can be used directly, or easily adapted. + + + + RSocket-JVM: streamlining implementation for each vendor platform + https://jauntsdn.com/post/rsocket-jvm/ + Thu, 22 Apr 2021 08:00:00 +0200 + + https://jauntsdn.com/post/rsocket-jvm/ + Motivation In practice reactive applications - as a method to model software and programming paradigm of composable flow controlled streams - viable mainly within JVM context. +It has necessary ecosystem provided by several competing platforms: smallrye-mutiny by Red Hat, project reactor by (ex-)Pivotal, and community-maintained rxjava. +Libraries are interoperable on same VM because their APIs semantically conform to reactive-streams specification, with 3 of 4 having binary compatibility - streams may be combined directly without writing any adapter code. + + + + Serving one million streams. Part 2. Double performance over original RSocket library + https://jauntsdn.com/post/rsocket-million-streams-2/ + Wed, 20 Jan 2021 10:00:00 +0200 + + https://jauntsdn.com/post/rsocket-million-streams-2/ + New year break is good time for continuing on topic of one million of streams - serving huge amount of RSocket streams simultaneously with single, mid-level commodity computer. +The original library gave in around 500k streams - mark where server was still stable, at whopping 500-600 MBytes/s allocation rate - unexpected numbers for RSockets just sending same byte buffer periodically, using library advertising zero-copy capability. +Interesting question is whether we hit natural limit, caused by current state of JVM runtime and libraries, or is a consequence of design choices driven at large by “ideological” and marketing motives that eventually resulted in implementation having significant parts happen to exist just for burning CPU cycles - both directly and through garbage collection? + + + + WebSockets over http/2: implementing RFC8441 with Netty + https://jauntsdn.com/post/netty-websocket-http2/ + Thu, 30 Jul 2020 11:00:00 +0200 + + https://jauntsdn.com/post/netty-websocket-http2/ + Today I’d like to introduce you to netty-websocket-http2 - implementation of websockets-over-http2, first release of which is about to land on the Maven Central. +Novel protocol graduated out of draft phase in September 2018, and is one of the smallest RFCs I’ve ever seen - just 4 pages of actual specification. +Despite tiny size It opens some interesting possibilities - for both clients and servers. +Protocol defines Extended CONNECT Method - mechanism for establishing byte stream tunnel over single http2 stream, and addresses biggest flaw of websocket over http1 - need for a separate tcp connection per websocket. + + + + Serving one million streams. Part 1. RSocket limits on the JVM + https://jauntsdn.com/post/rsocket-million-streams-1/ + Wed, 24 Jun 2020 19:30:00 +0200 + + https://jauntsdn.com/post/rsocket-million-streams-1/ + One of the interesting topics is estimation of RSocket limits in setup that approximates gateway serving tens of thousands clients. +There are numerous reports focusing on few streams over single connection, with emphasis on huge throughput. However this mode is not intrinsical for real server. Even though gateways are more likely to be cpu bound than IO bound, we can relax this assumption by making server logic trivial (just echo back incoming messages), to see how RSocket behaves under idealized conditions when major resource consumption is related to IO. + + + + Service concurrency limiting with RSocket request leases + https://jauntsdn.com/post/rsocket-lease-concurrency-limiting/ + Thu, 09 Apr 2020 17:00:00 +0200 + + https://jauntsdn.com/post/rsocket-lease-concurrency-limiting/ + This is first post in advanced applications of RSocket series, and topic for today is less known yet essential protocol feature - requests leasing. +We know that single RSocket stream is naturally bounded due to Reactive Streams semantics - there is no more in-flight messages than demanded with receiver Subscription. +However limiting outstanding messages per request is not useful without limiting requests concurrency. +For services, growing requests unbounded exhausts their backing resources and leads to latency surge. + + + + RSocket: short introduction + https://jauntsdn.com/post/rsocket-intro/ + Tue, 17 Mar 2020 11:30:00 +0200 + + https://jauntsdn.com/post/rsocket-intro/ + Description from official RSocket-java repository +RSocket is a binary protocol for use on byte stream transports... It enables the following symmetric interaction models via async message passing over a single connection.. leaves bits of mystery unless reader is interested enough to skim over lengthy FAQ and motivations documents. This post is one page size summary of distinctive RSocket features that make It a good companion for Http2/GRPC. +Low latency RSocket provides means for maintaining low latency under high throughput by limiting incoming and outgoing message queue size on both sides of connection: + + + + IO_URING for Netty based RPC: quick analysis + https://jauntsdn.com/post/rpc-iouring/ + Sat, 02 Mar 2019 08:00:00 +0200 + + https://jauntsdn.com/post/rpc-iouring/ + Netty IO_URING based transport was recently released as incubator project. Several sources have reported substantial performance gains with It, including Netty team TCP echo test results that demonstrated 2x better throughput compared to EPOLL. +The goal of this post is CPU and memory performance evaluation of IO_URING in RPC library implemented with Netty on top of TCP, and comparison against EPOLL IO. +It focuses on two use cases: + backend service: few dozens of connections, each serving ~1000 concurrent streams of few dozens messages each + + + + + https://jauntsdn.com/mstreams/presentation/plugin/notes/speaker-view/ + Mon, 01 Jan 0001 00:00:00 +0000 + + https://jauntsdn.com/mstreams/presentation/plugin/notes/speaker-view/ + reveal.js - Speaker View body { font-family: Helvetica; font-size: 18px; } #current-slide, #upcoming-slide, #speaker-controls { padding: 6px; box-sizing: border-box; -moz-box-sizing: border-box; } #current-slide iframe, #upcoming-slide iframe { width: 100%; height: 100%; border: 1px solid #ddd; } #current-slide .label, #upcoming-slide .label { position: absolute; top: 10px; left: 10px; z-index: 2; } #connection-status { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 20; padding: 30% 20% 20% 20%; font-size: 18px; color: #222; background: #fff; text-align: center; box-sizing: border-box; line-height: 1. + + + + \ No newline at end of file diff --git a/mstreams/index.html b/mstreams/index.html new file mode 100644 index 0000000..4bb62b4 --- /dev/null +++ b/mstreams/index.html @@ -0,0 +1,87 @@ + + + + + + + + + + + + Message-streams + + + + + +
+
+ +
TLDR. Message-Streams is very fast GRPC-like & GRPC-compatible services on JVM with rich streaming + models over multiple network transports [1].
+
MILLIONS. Millions of requests/messages per core + with each interaction model. Million of simultaneous streams served with commodity PC.
+
GRPC COMPATIBILITY. High-performance streaming with codegen/Protobuf based RPC system having GRPC + compatibility & look-and-feel.
+ +
MULTIPLE APIS. Request-response with CompletableFuture and virtual threads; traditional streaming with GRPC-stubs (StreamObserver), or reactive with smallrye-mutiny, rxjava, reactor [2].
+
PLUGGABLE NETWORKING. Data center: TCP, unix sockets, VM sockets; internet: GRPC, websockets, websockets-over-http2 — all 3 internet transports may be served over same port.
+
API / RUNTIME SEPARATION. Service APIs & RPC stubs (Message-Streams) are decoupled from library runtime. Latter also + includes shared modules: network transports, load estimators, load balancers and metrics [3].
+
EFFICIENT. Minimal per-message garbage production. Server graceful close support, coarse scheduler for timeouts, + lightweight keep-alives. Lean RPC and client metadata encoding.
+
LOAD BALANCING. Transparent origin (RPC) & proxy load estimation for cpu-efficient load + balancers.
+
TINY FOOTPRINT. Minimal service is <3MB, application image with JRE <50MB, alpine container <60MB.
+
NATIVE. Instant startup & even smaller binary footprint (~20MB), RSS memory with graalvm.
+ + +
EXAMPLES. + Message-Streams-RPC futures example is available at [4], + virtual threads RPC example - at [5], all libraries interop plus GRPC examples - at [6]. + More details in presentation at [7]. +
+ + +
+
+ + + + diff --git a/mstreams/index.xml b/mstreams/index.xml new file mode 100644 index 0000000..0405297 --- /dev/null +++ b/mstreams/index.xml @@ -0,0 +1,23 @@ + + + + JAUNT.SDN + https://jauntsdn.com/mstreams/ + Recent content on JAUNT.SDN + Hugo -- gohugo.io + en-us + + + + + + + https://jauntsdn.com/mstreams/presentation/plugin/notes/speaker-view/ + Mon, 01 Jan 0001 00:00:00 +0000 + + https://jauntsdn.com/mstreams/presentation/plugin/notes/speaker-view/ + reveal.js - Speaker View body { font-family: Helvetica; font-size: 18px; } #current-slide, #upcoming-slide, #speaker-controls { padding: 6px; box-sizing: border-box; -moz-box-sizing: border-box; } #current-slide iframe, #upcoming-slide iframe { width: 100%; height: 100%; border: 1px solid #ddd; } #current-slide .label, #upcoming-slide .label { position: absolute; top: 10px; left: 10px; z-index: 2; } #connection-status { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 20; padding: 30% 20% 20% 20%; font-size: 18px; color: #222; background: #fff; text-align: center; box-sizing: border-box; line-height: 1. + + + + \ No newline at end of file diff --git a/mstreams/presentation/dist/reset.css b/mstreams/presentation/dist/reset.css new file mode 100644 index 0000000..e238539 --- /dev/null +++ b/mstreams/presentation/dist/reset.css @@ -0,0 +1,30 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v4.0 | 20180602 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +main, menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, main, menu, nav, section { + display: block; +} \ No newline at end of file diff --git a/mstreams/presentation/dist/reveal.css b/mstreams/presentation/dist/reveal.css new file mode 100644 index 0000000..bf3824d --- /dev/null +++ b/mstreams/presentation/dist/reveal.css @@ -0,0 +1,8 @@ +/*! +* reveal.js 4.3.1 +* https://revealjs.com +* MIT licensed +* +* Copyright (C) 2011-2022 Hakim El Hattab, https://hakim.se +*/ +.reveal .r-stretch,.reveal .stretch{max-width:none;max-height:none}.reveal pre.r-stretch code,.reveal pre.stretch code{height:100%;max-height:100%;box-sizing:border-box}.reveal .r-fit-text{display:inline-block;white-space:nowrap}.reveal .r-stack{display:grid}.reveal .r-stack>*{grid-area:1/1;margin:auto}.reveal .r-hstack,.reveal .r-vstack{display:flex}.reveal .r-hstack img,.reveal .r-hstack video,.reveal .r-vstack img,.reveal .r-vstack video{min-width:0;min-height:0;-o-object-fit:contain;object-fit:contain}.reveal .r-vstack{flex-direction:column;align-items:center;justify-content:center}.reveal .r-hstack{flex-direction:row;align-items:center;justify-content:center}.reveal .items-stretch{align-items:stretch}.reveal .items-start{align-items:flex-start}.reveal .items-center{align-items:center}.reveal .items-end{align-items:flex-end}.reveal .justify-between{justify-content:space-between}.reveal .justify-around{justify-content:space-around}.reveal .justify-start{justify-content:flex-start}.reveal .justify-center{justify-content:center}.reveal .justify-end{justify-content:flex-end}html.reveal-full-page{width:100%;height:100%;height:100vh;height:calc(var(--vh,1vh) * 100);overflow:hidden}.reveal-viewport{height:100%;overflow:hidden;position:relative;line-height:1;margin:0;background-color:#fff;color:#000}.reveal-viewport:-webkit-full-screen{top:0!important;left:0!important;width:100%!important;height:100%!important;transform:none!important}.reveal-viewport:-ms-fullscreen{top:0!important;left:0!important;width:100%!important;height:100%!important;transform:none!important}.reveal-viewport:fullscreen{top:0!important;left:0!important;width:100%!important;height:100%!important;transform:none!important}.reveal .slides section .fragment{opacity:0;visibility:hidden;transition:all .2s ease;will-change:opacity}.reveal .slides section .fragment.visible{opacity:1;visibility:inherit}.reveal .slides section .fragment.disabled{transition:none}.reveal .slides section .fragment.grow{opacity:1;visibility:inherit}.reveal .slides section .fragment.grow.visible{transform:scale(1.3)}.reveal .slides section .fragment.shrink{opacity:1;visibility:inherit}.reveal .slides section .fragment.shrink.visible{transform:scale(.7)}.reveal .slides section .fragment.zoom-in{transform:scale(.1)}.reveal .slides section .fragment.zoom-in.visible{transform:none}.reveal .slides section .fragment.fade-out{opacity:1;visibility:inherit}.reveal .slides section .fragment.fade-out.visible{opacity:0;visibility:hidden}.reveal .slides section .fragment.semi-fade-out{opacity:1;visibility:inherit}.reveal .slides section .fragment.semi-fade-out.visible{opacity:.5;visibility:inherit}.reveal .slides section .fragment.strike{opacity:1;visibility:inherit}.reveal .slides section .fragment.strike.visible{text-decoration:line-through}.reveal .slides section .fragment.fade-up{transform:translate(0,40px)}.reveal .slides section .fragment.fade-up.visible{transform:translate(0,0)}.reveal .slides section .fragment.fade-down{transform:translate(0,-40px)}.reveal .slides section .fragment.fade-down.visible{transform:translate(0,0)}.reveal .slides section .fragment.fade-right{transform:translate(-40px,0)}.reveal .slides section .fragment.fade-right.visible{transform:translate(0,0)}.reveal .slides section .fragment.fade-left{transform:translate(40px,0)}.reveal .slides section .fragment.fade-left.visible{transform:translate(0,0)}.reveal .slides section .fragment.current-visible,.reveal .slides section .fragment.fade-in-then-out{opacity:0;visibility:hidden}.reveal .slides section .fragment.current-visible.current-fragment,.reveal .slides section .fragment.fade-in-then-out.current-fragment{opacity:1;visibility:inherit}.reveal .slides section .fragment.fade-in-then-semi-out{opacity:0;visibility:hidden}.reveal .slides section .fragment.fade-in-then-semi-out.visible{opacity:.5;visibility:inherit}.reveal .slides section .fragment.fade-in-then-semi-out.current-fragment{opacity:1;visibility:inherit}.reveal .slides section .fragment.highlight-blue,.reveal .slides section .fragment.highlight-current-blue,.reveal .slides section .fragment.highlight-current-green,.reveal .slides section .fragment.highlight-current-red,.reveal .slides section .fragment.highlight-green,.reveal .slides section .fragment.highlight-red{opacity:1;visibility:inherit}.reveal .slides section .fragment.highlight-red.visible{color:#ff2c2d}.reveal .slides section .fragment.highlight-green.visible{color:#17ff2e}.reveal .slides section .fragment.highlight-blue.visible{color:#1b91ff}.reveal .slides section .fragment.highlight-current-red.current-fragment{color:#ff2c2d}.reveal .slides section .fragment.highlight-current-green.current-fragment{color:#17ff2e}.reveal .slides section .fragment.highlight-current-blue.current-fragment{color:#1b91ff}.reveal:after{content:"";font-style:italic}.reveal iframe{z-index:1}.reveal a{position:relative}@keyframes bounce-right{0%,10%,25%,40%,50%{transform:translateX(0)}20%{transform:translateX(10px)}30%{transform:translateX(-5px)}}@keyframes bounce-left{0%,10%,25%,40%,50%{transform:translateX(0)}20%{transform:translateX(-10px)}30%{transform:translateX(5px)}}@keyframes bounce-down{0%,10%,25%,40%,50%{transform:translateY(0)}20%{transform:translateY(10px)}30%{transform:translateY(-5px)}}.reveal .controls{display:none;position:absolute;top:auto;bottom:12px;right:12px;left:auto;z-index:11;color:#000;pointer-events:none;font-size:10px}.reveal .controls button{position:absolute;padding:0;background-color:transparent;border:0;outline:0;cursor:pointer;color:currentColor;transform:scale(.9999);transition:color .2s ease,opacity .2s ease,transform .2s ease;z-index:2;pointer-events:auto;font-size:inherit;visibility:hidden;opacity:0;-webkit-appearance:none;-webkit-tap-highlight-color:transparent}.reveal .controls .controls-arrow:after,.reveal .controls .controls-arrow:before{content:"";position:absolute;top:0;left:0;width:2.6em;height:.5em;border-radius:.25em;background-color:currentColor;transition:all .15s ease,background-color .8s ease;transform-origin:.2em 50%;will-change:transform}.reveal .controls .controls-arrow{position:relative;width:3.6em;height:3.6em}.reveal .controls .controls-arrow:before{transform:translateX(.5em) translateY(1.55em) rotate(45deg)}.reveal .controls .controls-arrow:after{transform:translateX(.5em) translateY(1.55em) rotate(-45deg)}.reveal .controls .controls-arrow:hover:before{transform:translateX(.5em) translateY(1.55em) rotate(40deg)}.reveal .controls .controls-arrow:hover:after{transform:translateX(.5em) translateY(1.55em) rotate(-40deg)}.reveal .controls .controls-arrow:active:before{transform:translateX(.5em) translateY(1.55em) rotate(36deg)}.reveal .controls .controls-arrow:active:after{transform:translateX(.5em) translateY(1.55em) rotate(-36deg)}.reveal .controls .navigate-left{right:6.4em;bottom:3.2em;transform:translateX(-10px)}.reveal .controls .navigate-left.highlight{animation:bounce-left 2s 50 both ease-out}.reveal .controls .navigate-right{right:0;bottom:3.2em;transform:translateX(10px)}.reveal .controls .navigate-right .controls-arrow{transform:rotate(180deg)}.reveal .controls .navigate-right.highlight{animation:bounce-right 2s 50 both ease-out}.reveal .controls .navigate-up{right:3.2em;bottom:6.4em;transform:translateY(-10px)}.reveal .controls .navigate-up .controls-arrow{transform:rotate(90deg)}.reveal .controls .navigate-down{right:3.2em;bottom:-1.4em;padding-bottom:1.4em;transform:translateY(10px)}.reveal .controls .navigate-down .controls-arrow{transform:rotate(-90deg)}.reveal .controls .navigate-down.highlight{animation:bounce-down 2s 50 both ease-out}.reveal .controls[data-controls-back-arrows=faded] .navigate-up.enabled{opacity:.3}.reveal .controls[data-controls-back-arrows=faded] .navigate-up.enabled:hover{opacity:1}.reveal .controls[data-controls-back-arrows=hidden] .navigate-up.enabled{opacity:0;visibility:hidden}.reveal .controls .enabled{visibility:visible;opacity:.9;cursor:pointer;transform:none}.reveal .controls .enabled.fragmented{opacity:.5}.reveal .controls .enabled.fragmented:hover,.reveal .controls .enabled:hover{opacity:1}.reveal:not(.rtl) .controls[data-controls-back-arrows=faded] .navigate-left.enabled{opacity:.3}.reveal:not(.rtl) .controls[data-controls-back-arrows=faded] .navigate-left.enabled:hover{opacity:1}.reveal:not(.rtl) .controls[data-controls-back-arrows=hidden] .navigate-left.enabled{opacity:0;visibility:hidden}.reveal.rtl .controls[data-controls-back-arrows=faded] .navigate-right.enabled{opacity:.3}.reveal.rtl .controls[data-controls-back-arrows=faded] .navigate-right.enabled:hover{opacity:1}.reveal.rtl .controls[data-controls-back-arrows=hidden] .navigate-right.enabled{opacity:0;visibility:hidden}.reveal[data-navigation-mode=linear].has-horizontal-slides .navigate-down,.reveal[data-navigation-mode=linear].has-horizontal-slides .navigate-up{display:none}.reveal:not(.has-vertical-slides) .controls .navigate-left,.reveal[data-navigation-mode=linear].has-horizontal-slides .navigate-left{bottom:1.4em;right:5.5em}.reveal:not(.has-vertical-slides) .controls .navigate-right,.reveal[data-navigation-mode=linear].has-horizontal-slides .navigate-right{bottom:1.4em;right:.5em}.reveal:not(.has-horizontal-slides) .controls .navigate-up{right:1.4em;bottom:5em}.reveal:not(.has-horizontal-slides) .controls .navigate-down{right:1.4em;bottom:.5em}.reveal.has-dark-background .controls{color:#fff}.reveal.has-light-background .controls{color:#000}.reveal.no-hover .controls .controls-arrow:active:before,.reveal.no-hover .controls .controls-arrow:hover:before{transform:translateX(.5em) translateY(1.55em) rotate(45deg)}.reveal.no-hover .controls .controls-arrow:active:after,.reveal.no-hover .controls .controls-arrow:hover:after{transform:translateX(.5em) translateY(1.55em) rotate(-45deg)}@media screen and (min-width:500px){.reveal .controls[data-controls-layout=edges]{top:0;right:0;bottom:0;left:0}.reveal .controls[data-controls-layout=edges] .navigate-down,.reveal .controls[data-controls-layout=edges] .navigate-left,.reveal .controls[data-controls-layout=edges] .navigate-right,.reveal .controls[data-controls-layout=edges] .navigate-up{bottom:auto;right:auto}.reveal .controls[data-controls-layout=edges] .navigate-left{top:50%;left:.8em;margin-top:-1.8em}.reveal .controls[data-controls-layout=edges] .navigate-right{top:50%;right:.8em;margin-top:-1.8em}.reveal .controls[data-controls-layout=edges] .navigate-up{top:.8em;left:50%;margin-left:-1.8em}.reveal .controls[data-controls-layout=edges] .navigate-down{bottom:-.3em;left:50%;margin-left:-1.8em}}.reveal .progress{position:absolute;display:none;height:3px;width:100%;bottom:0;left:0;z-index:10;background-color:rgba(0,0,0,.2);color:#fff}.reveal .progress:after{content:"";display:block;position:absolute;height:10px;width:100%;top:-10px}.reveal .progress span{display:block;height:100%;width:100%;background-color:currentColor;transition:transform .8s cubic-bezier(.26,.86,.44,.985);transform-origin:0 0;transform:scaleX(0)}.reveal .slide-number{position:absolute;display:block;right:8px;bottom:8px;z-index:31;font-family:Helvetica,sans-serif;font-size:12px;line-height:1;color:#fff;background-color:rgba(0,0,0,.4);padding:5px}.reveal .slide-number a{color:currentColor}.reveal .slide-number-delimiter{margin:0 3px}.reveal{position:relative;width:100%;height:100%;overflow:hidden;touch-action:pinch-zoom}.reveal.embedded{touch-action:pan-y}.reveal .slides{position:absolute;width:100%;height:100%;top:0;right:0;bottom:0;left:0;margin:auto;pointer-events:none;overflow:visible;z-index:1;text-align:center;perspective:600px;perspective-origin:50% 40%}.reveal .slides>section{perspective:600px}.reveal .slides>section,.reveal .slides>section>section{display:none;position:absolute;width:100%;pointer-events:auto;z-index:10;transform-style:flat;transition:transform-origin .8s cubic-bezier(.26,.86,.44,.985),transform .8s cubic-bezier(.26,.86,.44,.985),visibility .8s cubic-bezier(.26,.86,.44,.985),opacity .8s cubic-bezier(.26,.86,.44,.985)}.reveal[data-transition-speed=fast] .slides section{transition-duration:.4s}.reveal[data-transition-speed=slow] .slides section{transition-duration:1.2s}.reveal .slides section[data-transition-speed=fast]{transition-duration:.4s}.reveal .slides section[data-transition-speed=slow]{transition-duration:1.2s}.reveal .slides>section.stack{padding-top:0;padding-bottom:0;pointer-events:none;height:100%}.reveal .slides>section.present,.reveal .slides>section>section.present{display:block;z-index:11;opacity:1}.reveal .slides>section:empty,.reveal .slides>section>section:empty,.reveal .slides>section>section[data-background-interactive],.reveal .slides>section[data-background-interactive]{pointer-events:none}.reveal.center,.reveal.center .slides,.reveal.center .slides section{min-height:0!important}.reveal .slides>section:not(.present),.reveal .slides>section>section:not(.present){pointer-events:none}.reveal.overview .slides>section,.reveal.overview .slides>section>section{pointer-events:auto}.reveal .slides>section.future,.reveal .slides>section.past,.reveal .slides>section>section.future,.reveal .slides>section>section.past{opacity:0}.reveal .slides>section[data-transition=slide].past,.reveal .slides>section[data-transition~=slide-out].past,.reveal.slide .slides>section:not([data-transition]).past{transform:translate(-150%,0)}.reveal .slides>section[data-transition=slide].future,.reveal .slides>section[data-transition~=slide-in].future,.reveal.slide .slides>section:not([data-transition]).future{transform:translate(150%,0)}.reveal .slides>section>section[data-transition=slide].past,.reveal .slides>section>section[data-transition~=slide-out].past,.reveal.slide .slides>section>section:not([data-transition]).past{transform:translate(0,-150%)}.reveal .slides>section>section[data-transition=slide].future,.reveal .slides>section>section[data-transition~=slide-in].future,.reveal.slide .slides>section>section:not([data-transition]).future{transform:translate(0,150%)}.reveal .slides>section[data-transition=linear].past,.reveal .slides>section[data-transition~=linear-out].past,.reveal.linear .slides>section:not([data-transition]).past{transform:translate(-150%,0)}.reveal .slides>section[data-transition=linear].future,.reveal .slides>section[data-transition~=linear-in].future,.reveal.linear .slides>section:not([data-transition]).future{transform:translate(150%,0)}.reveal .slides>section>section[data-transition=linear].past,.reveal .slides>section>section[data-transition~=linear-out].past,.reveal.linear .slides>section>section:not([data-transition]).past{transform:translate(0,-150%)}.reveal .slides>section>section[data-transition=linear].future,.reveal .slides>section>section[data-transition~=linear-in].future,.reveal.linear .slides>section>section:not([data-transition]).future{transform:translate(0,150%)}.reveal .slides section[data-transition=default].stack,.reveal.default .slides section.stack{transform-style:preserve-3d}.reveal .slides>section[data-transition=default].past,.reveal .slides>section[data-transition~=default-out].past,.reveal.default .slides>section:not([data-transition]).past{transform:translate3d(-100%,0,0) rotateY(-90deg) translate3d(-100%,0,0)}.reveal .slides>section[data-transition=default].future,.reveal .slides>section[data-transition~=default-in].future,.reveal.default .slides>section:not([data-transition]).future{transform:translate3d(100%,0,0) rotateY(90deg) translate3d(100%,0,0)}.reveal .slides>section>section[data-transition=default].past,.reveal .slides>section>section[data-transition~=default-out].past,.reveal.default .slides>section>section:not([data-transition]).past{transform:translate3d(0,-300px,0) rotateX(70deg) translate3d(0,-300px,0)}.reveal .slides>section>section[data-transition=default].future,.reveal .slides>section>section[data-transition~=default-in].future,.reveal.default .slides>section>section:not([data-transition]).future{transform:translate3d(0,300px,0) rotateX(-70deg) translate3d(0,300px,0)}.reveal .slides section[data-transition=convex].stack,.reveal.convex .slides section.stack{transform-style:preserve-3d}.reveal .slides>section[data-transition=convex].past,.reveal .slides>section[data-transition~=convex-out].past,.reveal.convex .slides>section:not([data-transition]).past{transform:translate3d(-100%,0,0) rotateY(-90deg) translate3d(-100%,0,0)}.reveal .slides>section[data-transition=convex].future,.reveal .slides>section[data-transition~=convex-in].future,.reveal.convex .slides>section:not([data-transition]).future{transform:translate3d(100%,0,0) rotateY(90deg) translate3d(100%,0,0)}.reveal .slides>section>section[data-transition=convex].past,.reveal .slides>section>section[data-transition~=convex-out].past,.reveal.convex .slides>section>section:not([data-transition]).past{transform:translate3d(0,-300px,0) rotateX(70deg) translate3d(0,-300px,0)}.reveal .slides>section>section[data-transition=convex].future,.reveal .slides>section>section[data-transition~=convex-in].future,.reveal.convex .slides>section>section:not([data-transition]).future{transform:translate3d(0,300px,0) rotateX(-70deg) translate3d(0,300px,0)}.reveal .slides section[data-transition=concave].stack,.reveal.concave .slides section.stack{transform-style:preserve-3d}.reveal .slides>section[data-transition=concave].past,.reveal .slides>section[data-transition~=concave-out].past,.reveal.concave .slides>section:not([data-transition]).past{transform:translate3d(-100%,0,0) rotateY(90deg) translate3d(-100%,0,0)}.reveal .slides>section[data-transition=concave].future,.reveal .slides>section[data-transition~=concave-in].future,.reveal.concave .slides>section:not([data-transition]).future{transform:translate3d(100%,0,0) rotateY(-90deg) translate3d(100%,0,0)}.reveal .slides>section>section[data-transition=concave].past,.reveal .slides>section>section[data-transition~=concave-out].past,.reveal.concave .slides>section>section:not([data-transition]).past{transform:translate3d(0,-80%,0) rotateX(-70deg) translate3d(0,-80%,0)}.reveal .slides>section>section[data-transition=concave].future,.reveal .slides>section>section[data-transition~=concave-in].future,.reveal.concave .slides>section>section:not([data-transition]).future{transform:translate3d(0,80%,0) rotateX(70deg) translate3d(0,80%,0)}.reveal .slides section[data-transition=zoom],.reveal.zoom .slides section:not([data-transition]){transition-timing-function:ease}.reveal .slides>section[data-transition=zoom].past,.reveal .slides>section[data-transition~=zoom-out].past,.reveal.zoom .slides>section:not([data-transition]).past{visibility:hidden;transform:scale(16)}.reveal .slides>section[data-transition=zoom].future,.reveal .slides>section[data-transition~=zoom-in].future,.reveal.zoom .slides>section:not([data-transition]).future{visibility:hidden;transform:scale(.2)}.reveal .slides>section>section[data-transition=zoom].past,.reveal .slides>section>section[data-transition~=zoom-out].past,.reveal.zoom .slides>section>section:not([data-transition]).past{transform:scale(16)}.reveal .slides>section>section[data-transition=zoom].future,.reveal .slides>section>section[data-transition~=zoom-in].future,.reveal.zoom .slides>section>section:not([data-transition]).future{transform:scale(.2)}.reveal.cube .slides{perspective:1300px}.reveal.cube .slides section{padding:30px;min-height:700px;-webkit-backface-visibility:hidden;backface-visibility:hidden;box-sizing:border-box;transform-style:preserve-3d}.reveal.center.cube .slides section{min-height:0}.reveal.cube .slides section:not(.stack):before{content:"";position:absolute;display:block;width:100%;height:100%;left:0;top:0;background:rgba(0,0,0,.1);border-radius:4px;transform:translateZ(-20px)}.reveal.cube .slides section:not(.stack):after{content:"";position:absolute;display:block;width:90%;height:30px;left:5%;bottom:0;background:0 0;z-index:1;border-radius:4px;box-shadow:0 95px 25px rgba(0,0,0,.2);transform:translateZ(-90px) rotateX(65deg)}.reveal.cube .slides>section.stack{padding:0;background:0 0}.reveal.cube .slides>section.past{transform-origin:100% 0;transform:translate3d(-100%,0,0) rotateY(-90deg)}.reveal.cube .slides>section.future{transform-origin:0 0;transform:translate3d(100%,0,0) rotateY(90deg)}.reveal.cube .slides>section>section.past{transform-origin:0 100%;transform:translate3d(0,-100%,0) rotateX(90deg)}.reveal.cube .slides>section>section.future{transform-origin:0 0;transform:translate3d(0,100%,0) rotateX(-90deg)}.reveal.page .slides{perspective-origin:0 50%;perspective:3000px}.reveal.page .slides section{padding:30px;min-height:700px;box-sizing:border-box;transform-style:preserve-3d}.reveal.page .slides section.past{z-index:12}.reveal.page .slides section:not(.stack):before{content:"";position:absolute;display:block;width:100%;height:100%;left:0;top:0;background:rgba(0,0,0,.1);transform:translateZ(-20px)}.reveal.page .slides section:not(.stack):after{content:"";position:absolute;display:block;width:90%;height:30px;left:5%;bottom:0;background:0 0;z-index:1;border-radius:4px;box-shadow:0 95px 25px rgba(0,0,0,.2);-webkit-transform:translateZ(-90px) rotateX(65deg)}.reveal.page .slides>section.stack{padding:0;background:0 0}.reveal.page .slides>section.past{transform-origin:0 0;transform:translate3d(-40%,0,0) rotateY(-80deg)}.reveal.page .slides>section.future{transform-origin:100% 0;transform:translate3d(0,0,0)}.reveal.page .slides>section>section.past{transform-origin:0 0;transform:translate3d(0,-40%,0) rotateX(80deg)}.reveal.page .slides>section>section.future{transform-origin:0 100%;transform:translate3d(0,0,0)}.reveal .slides section[data-transition=fade],.reveal.fade .slides section:not([data-transition]),.reveal.fade .slides>section>section:not([data-transition]){transform:none;transition:opacity .5s}.reveal.fade.overview .slides section,.reveal.fade.overview .slides>section>section{transition:none}.reveal .slides section[data-transition=none],.reveal.none .slides section:not([data-transition]){transform:none;transition:none}.reveal .pause-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:#000;visibility:hidden;opacity:0;z-index:100;transition:all 1s ease}.reveal .pause-overlay .resume-button{position:absolute;bottom:20px;right:20px;color:#ccc;border-radius:2px;padding:6px 14px;border:2px solid #ccc;font-size:16px;background:0 0;cursor:pointer}.reveal .pause-overlay .resume-button:hover{color:#fff;border-color:#fff}.reveal.paused .pause-overlay{visibility:visible;opacity:1}.reveal .no-transition,.reveal .no-transition *,.reveal .slides.disable-slide-transitions section{transition:none!important}.reveal .slides.disable-slide-transitions section{transform:none!important}.reveal .backgrounds{position:absolute;width:100%;height:100%;top:0;left:0;perspective:600px}.reveal .slide-background{display:none;position:absolute;width:100%;height:100%;opacity:0;visibility:hidden;overflow:hidden;background-color:rgba(0,0,0,0);transition:all .8s cubic-bezier(.26,.86,.44,.985)}.reveal .slide-background-content{position:absolute;width:100%;height:100%;background-position:50% 50%;background-repeat:no-repeat;background-size:cover}.reveal .slide-background.stack{display:block}.reveal .slide-background.present{opacity:1;visibility:visible;z-index:2}.print-pdf .reveal .slide-background{opacity:1!important;visibility:visible!important}.reveal .slide-background video{position:absolute;width:100%;height:100%;max-width:none;max-height:none;top:0;left:0;-o-object-fit:cover;object-fit:cover}.reveal .slide-background[data-background-size=contain] video{-o-object-fit:contain;object-fit:contain}.reveal>.backgrounds .slide-background[data-background-transition=none],.reveal[data-background-transition=none]>.backgrounds .slide-background:not([data-background-transition]){transition:none}.reveal>.backgrounds .slide-background[data-background-transition=slide],.reveal[data-background-transition=slide]>.backgrounds .slide-background:not([data-background-transition]){opacity:1}.reveal>.backgrounds .slide-background.past[data-background-transition=slide],.reveal[data-background-transition=slide]>.backgrounds .slide-background.past:not([data-background-transition]){transform:translate(-100%,0)}.reveal>.backgrounds .slide-background.future[data-background-transition=slide],.reveal[data-background-transition=slide]>.backgrounds .slide-background.future:not([data-background-transition]){transform:translate(100%,0)}.reveal>.backgrounds .slide-background>.slide-background.past[data-background-transition=slide],.reveal[data-background-transition=slide]>.backgrounds .slide-background>.slide-background.past:not([data-background-transition]){transform:translate(0,-100%)}.reveal>.backgrounds .slide-background>.slide-background.future[data-background-transition=slide],.reveal[data-background-transition=slide]>.backgrounds .slide-background>.slide-background.future:not([data-background-transition]){transform:translate(0,100%)}.reveal>.backgrounds .slide-background.past[data-background-transition=convex],.reveal[data-background-transition=convex]>.backgrounds .slide-background.past:not([data-background-transition]){opacity:0;transform:translate3d(-100%,0,0) rotateY(-90deg) translate3d(-100%,0,0)}.reveal>.backgrounds .slide-background.future[data-background-transition=convex],.reveal[data-background-transition=convex]>.backgrounds .slide-background.future:not([data-background-transition]){opacity:0;transform:translate3d(100%,0,0) rotateY(90deg) translate3d(100%,0,0)}.reveal>.backgrounds .slide-background>.slide-background.past[data-background-transition=convex],.reveal[data-background-transition=convex]>.backgrounds .slide-background>.slide-background.past:not([data-background-transition]){opacity:0;transform:translate3d(0,-100%,0) rotateX(90deg) translate3d(0,-100%,0)}.reveal>.backgrounds .slide-background>.slide-background.future[data-background-transition=convex],.reveal[data-background-transition=convex]>.backgrounds .slide-background>.slide-background.future:not([data-background-transition]){opacity:0;transform:translate3d(0,100%,0) rotateX(-90deg) translate3d(0,100%,0)}.reveal>.backgrounds .slide-background.past[data-background-transition=concave],.reveal[data-background-transition=concave]>.backgrounds .slide-background.past:not([data-background-transition]){opacity:0;transform:translate3d(-100%,0,0) rotateY(90deg) translate3d(-100%,0,0)}.reveal>.backgrounds .slide-background.future[data-background-transition=concave],.reveal[data-background-transition=concave]>.backgrounds .slide-background.future:not([data-background-transition]){opacity:0;transform:translate3d(100%,0,0) rotateY(-90deg) translate3d(100%,0,0)}.reveal>.backgrounds .slide-background>.slide-background.past[data-background-transition=concave],.reveal[data-background-transition=concave]>.backgrounds .slide-background>.slide-background.past:not([data-background-transition]){opacity:0;transform:translate3d(0,-100%,0) rotateX(-90deg) translate3d(0,-100%,0)}.reveal>.backgrounds .slide-background>.slide-background.future[data-background-transition=concave],.reveal[data-background-transition=concave]>.backgrounds .slide-background>.slide-background.future:not([data-background-transition]){opacity:0;transform:translate3d(0,100%,0) rotateX(90deg) translate3d(0,100%,0)}.reveal>.backgrounds .slide-background[data-background-transition=zoom],.reveal[data-background-transition=zoom]>.backgrounds .slide-background:not([data-background-transition]){transition-timing-function:ease}.reveal>.backgrounds .slide-background.past[data-background-transition=zoom],.reveal[data-background-transition=zoom]>.backgrounds .slide-background.past:not([data-background-transition]){opacity:0;visibility:hidden;transform:scale(16)}.reveal>.backgrounds .slide-background.future[data-background-transition=zoom],.reveal[data-background-transition=zoom]>.backgrounds .slide-background.future:not([data-background-transition]){opacity:0;visibility:hidden;transform:scale(.2)}.reveal>.backgrounds .slide-background>.slide-background.past[data-background-transition=zoom],.reveal[data-background-transition=zoom]>.backgrounds .slide-background>.slide-background.past:not([data-background-transition]){opacity:0;visibility:hidden;transform:scale(16)}.reveal>.backgrounds .slide-background>.slide-background.future[data-background-transition=zoom],.reveal[data-background-transition=zoom]>.backgrounds .slide-background>.slide-background.future:not([data-background-transition]){opacity:0;visibility:hidden;transform:scale(.2)}.reveal[data-transition-speed=fast]>.backgrounds .slide-background{transition-duration:.4s}.reveal[data-transition-speed=slow]>.backgrounds .slide-background{transition-duration:1.2s}.reveal [data-auto-animate-target^=unmatched]{will-change:opacity}.reveal section[data-auto-animate]:not(.stack):not([data-auto-animate=running]) [data-auto-animate-target^=unmatched]{opacity:0}.reveal.overview{perspective-origin:50% 50%;perspective:700px}.reveal.overview .slides{-moz-transform-style:preserve-3d}.reveal.overview .slides section{height:100%;top:0!important;opacity:1!important;overflow:hidden;visibility:visible!important;cursor:pointer;box-sizing:border-box}.reveal.overview .slides section.present,.reveal.overview .slides section:hover{outline:10px solid rgba(150,150,150,.4);outline-offset:10px}.reveal.overview .slides section .fragment{opacity:1;transition:none}.reveal.overview .slides section:after,.reveal.overview .slides section:before{display:none!important}.reveal.overview .slides>section.stack{padding:0;top:0!important;background:0 0;outline:0;overflow:visible}.reveal.overview .backgrounds{perspective:inherit;-moz-transform-style:preserve-3d}.reveal.overview .backgrounds .slide-background{opacity:1;visibility:visible;outline:10px solid rgba(150,150,150,.1);outline-offset:10px}.reveal.overview .backgrounds .slide-background.stack{overflow:visible}.reveal.overview .slides section,.reveal.overview-deactivating .slides section{transition:none}.reveal.overview .backgrounds .slide-background,.reveal.overview-deactivating .backgrounds .slide-background{transition:none}.reveal.rtl .slides,.reveal.rtl .slides h1,.reveal.rtl .slides h2,.reveal.rtl .slides h3,.reveal.rtl .slides h4,.reveal.rtl .slides h5,.reveal.rtl .slides h6{direction:rtl;font-family:sans-serif}.reveal.rtl code,.reveal.rtl pre{direction:ltr}.reveal.rtl ol,.reveal.rtl ul{text-align:right}.reveal.rtl .progress span{transform-origin:100% 0}.reveal.has-parallax-background .backgrounds{transition:all .8s ease}.reveal.has-parallax-background[data-transition-speed=fast] .backgrounds{transition-duration:.4s}.reveal.has-parallax-background[data-transition-speed=slow] .backgrounds{transition-duration:1.2s}.reveal>.overlay{position:absolute;top:0;left:0;width:100%;height:100%;z-index:1000;background:rgba(0,0,0,.9);transition:all .3s ease}.reveal>.overlay .spinner{position:absolute;display:block;top:50%;left:50%;width:32px;height:32px;margin:-16px 0 0 -16px;z-index:10;background-image:url(data:image/gif;base64,R0lGODlhIAAgAPMAAJmZmf%2F%2F%2F6%2Bvr8nJybW1tcDAwOjo6Nvb26ioqKOjo7Ozs%2FLy8vz8%2FAAAAAAAAAAAACH%2FC05FVFNDQVBFMi4wAwEAAAAh%2FhpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh%2BQQJCgAAACwAAAAAIAAgAAAE5xDISWlhperN52JLhSSdRgwVo1ICQZRUsiwHpTJT4iowNS8vyW2icCF6k8HMMBkCEDskxTBDAZwuAkkqIfxIQyhBQBFvAQSDITM5VDW6XNE4KagNh6Bgwe60smQUB3d4Rz1ZBApnFASDd0hihh12BkE9kjAJVlycXIg7CQIFA6SlnJ87paqbSKiKoqusnbMdmDC2tXQlkUhziYtyWTxIfy6BE8WJt5YJvpJivxNaGmLHT0VnOgSYf0dZXS7APdpB309RnHOG5gDqXGLDaC457D1zZ%2FV%2FnmOM82XiHRLYKhKP1oZmADdEAAAh%2BQQJCgAAACwAAAAAIAAgAAAE6hDISWlZpOrNp1lGNRSdRpDUolIGw5RUYhhHukqFu8DsrEyqnWThGvAmhVlteBvojpTDDBUEIFwMFBRAmBkSgOrBFZogCASwBDEY%2FCZSg7GSE0gSCjQBMVG023xWBhklAnoEdhQEfyNqMIcKjhRsjEdnezB%2BA4k8gTwJhFuiW4dokXiloUepBAp5qaKpp6%2BHo7aWW54wl7obvEe0kRuoplCGepwSx2jJvqHEmGt6whJpGpfJCHmOoNHKaHx61WiSR92E4lbFoq%2BB6QDtuetcaBPnW6%2BO7wDHpIiK9SaVK5GgV543tzjgGcghAgAh%2BQQJCgAAACwAAAAAIAAgAAAE7hDISSkxpOrN5zFHNWRdhSiVoVLHspRUMoyUakyEe8PTPCATW9A14E0UvuAKMNAZKYUZCiBMuBakSQKG8G2FzUWox2AUtAQFcBKlVQoLgQReZhQlCIJesQXI5B0CBnUMOxMCenoCfTCEWBsJColTMANldx15BGs8B5wlCZ9Po6OJkwmRpnqkqnuSrayqfKmqpLajoiW5HJq7FL1Gr2mMMcKUMIiJgIemy7xZtJsTmsM4xHiKv5KMCXqfyUCJEonXPN2rAOIAmsfB3uPoAK%2B%2BG%2Bw48edZPK%2BM6hLJpQg484enXIdQFSS1u6UhksENEQAAIfkECQoAAAAsAAAAACAAIAAABOcQyEmpGKLqzWcZRVUQnZYg1aBSh2GUVEIQ2aQOE%2BG%2BcD4ntpWkZQj1JIiZIogDFFyHI0UxQwFugMSOFIPJftfVAEoZLBbcLEFhlQiqGp1Vd140AUklUN3eCA51C1EWMzMCezCBBmkxVIVHBWd3HHl9JQOIJSdSnJ0TDKChCwUJjoWMPaGqDKannasMo6WnM562R5YluZRwur0wpgqZE7NKUm%2BFNRPIhjBJxKZteWuIBMN4zRMIVIhffcgojwCF117i4nlLnY5ztRLsnOk%2BaV%2BoJY7V7m76PdkS4trKcdg0Zc0tTcKkRAAAIfkECQoAAAAsAAAAACAAIAAABO4QyEkpKqjqzScpRaVkXZWQEximw1BSCUEIlDohrft6cpKCk5xid5MNJTaAIkekKGQkWyKHkvhKsR7ARmitkAYDYRIbUQRQjWBwJRzChi9CRlBcY1UN4g0%2FVNB0AlcvcAYHRyZPdEQFYV8ccwR5HWxEJ02YmRMLnJ1xCYp0Y5idpQuhopmmC2KgojKasUQDk5BNAwwMOh2RtRq5uQuPZKGIJQIGwAwGf6I0JXMpC8C7kXWDBINFMxS4DKMAWVWAGYsAdNqW5uaRxkSKJOZKaU3tPOBZ4DuK2LATgJhkPJMgTwKCdFjyPHEnKxFCDhEAACH5BAkKAAAALAAAAAAgACAAAATzEMhJaVKp6s2nIkolIJ2WkBShpkVRWqqQrhLSEu9MZJKK9y1ZrqYK9WiClmvoUaF8gIQSNeF1Er4MNFn4SRSDARWroAIETg1iVwuHjYB1kYc1mwruwXKC9gmsJXliGxc%2BXiUCby9ydh1sOSdMkpMTBpaXBzsfhoc5l58Gm5yToAaZhaOUqjkDgCWNHAULCwOLaTmzswadEqggQwgHuQsHIoZCHQMMQgQGubVEcxOPFAcMDAYUA85eWARmfSRQCdcMe0zeP1AAygwLlJtPNAAL19DARdPzBOWSm1brJBi45soRAWQAAkrQIykShQ9wVhHCwCQCACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq%2BE71SRQeyqUToLA7VxF0JDyIQh%2FMVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiRMDjI0Fd30%2FiI2UA5GSS5UDj2l6NoqgOgN4gksEBgYFf0FDqKgHnyZ9OX8HrgYHdHpcHQULXAS2qKpENRg7eAMLC7kTBaixUYFkKAzWAAnLC7FLVxLWDBLKCwaKTULgEwbLA4hJtOkSBNqITT3xEgfLpBtzE%2FjiuL04RGEBgwWhShRgQExHBAAh%2BQQJCgAAACwAAAAAIAAgAAAE7xDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfZiCqGk5dTESJeaOAlClzsJsqwiJwiqnFrb2nS9kmIcgEsjQydLiIlHehhpejaIjzh9eomSjZR%2BipslWIRLAgMDOR2DOqKogTB9pCUJBagDBXR6XB0EBkIIsaRsGGMMAxoDBgYHTKJiUYEGDAzHC9EACcUGkIgFzgwZ0QsSBcXHiQvOwgDdEwfFs0sDzt4S6BK4xYjkDOzn0unFeBzOBijIm1Dgmg5YFQwsCMjp1oJ8LyIAACH5BAkKAAAALAAAAAAgACAAAATwEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq%2BE71SRQeyqUToLA7VxF0JDyIQh%2FMVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GGl6NoiPOH16iZKNlH6KmyWFOggHhEEvAwwMA0N9GBsEC6amhnVcEwavDAazGwIDaH1ipaYLBUTCGgQDA8NdHz0FpqgTBwsLqAbWAAnIA4FWKdMLGdYGEgraigbT0OITBcg5QwPT4xLrROZL6AuQAPUS7bxLpoWidY0JtxLHKhwwMJBTHgPKdEQAACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq%2BE71SRQeyqUToLA7VxF0JDyIQh%2FMVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GAULDJCRiXo1CpGXDJOUjY%2BYip9DhToJA4RBLwMLCwVDfRgbBAaqqoZ1XBMHswsHtxtFaH1iqaoGNgAIxRpbFAgfPQSqpbgGBqUD1wBXeCYp1AYZ19JJOYgH1KwA4UBvQwXUBxPqVD9L3sbp2BNk2xvvFPJd%2BMFCN6HAAIKgNggY0KtEBAAh%2BQQJCgAAACwAAAAAIAAgAAAE6BDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfYIDMaAFdTESJeaEDAIMxYFqrOUaNW4E4ObYcCXaiBVEgULe0NJaxxtYksjh2NLkZISgDgJhHthkpU4mW6blRiYmZOlh4JWkDqILwUGBnE6TYEbCgevr0N1gH4At7gHiRpFaLNrrq8HNgAJA70AWxQIH1%2BvsYMDAzZQPC9VCNkDWUhGkuE5PxJNwiUK4UfLzOlD4WvzAHaoG9nxPi5d%2BjYUqfAhhykOFwJWiAAAIfkECQoAAAAsAAAAACAAIAAABPAQyElpUqnqzaciSoVkXVUMFaFSwlpOCcMYlErAavhOMnNLNo8KsZsMZItJEIDIFSkLGQoQTNhIsFehRww2CQLKF0tYGKYSg%2BygsZIuNqJksKgbfgIGepNo2cIUB3V1B3IvNiBYNQaDSTtfhhx0CwVPI0UJe0%2Bbm4g5VgcGoqOcnjmjqDSdnhgEoamcsZuXO1aWQy8KAwOAuTYYGwi7w5h%2BKr0SJ8MFihpNbx%2B4Erq7BYBuzsdiH1jCAzoSfl0rVirNbRXlBBlLX%2BBP0XJLAPGzTkAuAOqb0WT5AH7OcdCm5B8TgRwSRKIHQtaLCwg1RAAAOwAAAAAAAAAAAA%3D%3D);visibility:visible;opacity:.6;transition:all .3s ease}.reveal>.overlay header{position:absolute;left:0;top:0;width:100%;padding:5px;z-index:2;box-sizing:border-box}.reveal>.overlay header a{display:inline-block;width:40px;height:40px;line-height:36px;padding:0 10px;float:right;opacity:.6;box-sizing:border-box}.reveal>.overlay header a:hover{opacity:1}.reveal>.overlay header a .icon{display:inline-block;width:20px;height:20px;background-position:50% 50%;background-size:100%;background-repeat:no-repeat}.reveal>.overlay header a.close .icon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABkklEQVRYR8WX4VHDMAxG6wnoJrABZQPYBCaBTWAD2g1gE5gg6OOsXuxIlr40d81dfrSJ9V4c2VLK7spHuTJ/5wpM07QXuXc5X0opX2tEJcadjHuV80li/FgxTIEK/5QBCICBD6xEhSMGHgQPgBgLiYVAB1dpSqKDawxTohFw4JSEA3clzgIBPCURwE2JucBR7rhPJJv5OpJwDX+SfDjgx1wACQeJG1aChP9K/IMmdZ8DtESV1WyP3Bt4MwM6sj4NMxMYiqUWHQu4KYA/SYkIjOsm3BXYWMKFDwU2khjCQ4ELJUJ4SmClRArOCmSXGuKma0fYD5CbzHxFpCSGAhfAVSSUGDUk2BWZaff2g6GE15BsBQ9nwmpIGDiyHQddwNTMKkbZaf9fajXQca1EX44puJZUsnY0ObGmITE3GVLCbEhQUjGVt146j6oasWN+49Vph2w1pZ5EansNZqKBm1txbU57iRRcZ86RWMDdWtBJUHBHwoQPi1GV+JCbntmvok7iTX4/Up9mgyTc/FJYDTcndgH/AA5A/CHsyEkVAAAAAElFTkSuQmCC)}.reveal>.overlay header a.external .icon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAcElEQVRYR+2WSQoAIQwEzf8f7XiOMkUQxUPlGkM3hVmiQfQR9GYnH1SsAQlI4DiBqkCMoNb9y2e90IAEJPAcgdznU9+engMaeJ7Azh5Y1U67gAho4DqBqmB1buAf0MB1AlVBek83ZPkmJMGc1wAR+AAqod/B97TRpQAAAABJRU5ErkJggg==)}.reveal>.overlay .viewport{position:absolute;display:flex;top:50px;right:0;bottom:0;left:0}.reveal>.overlay.overlay-preview .viewport iframe{width:100%;height:100%;max-width:100%;max-height:100%;border:0;opacity:0;visibility:hidden;transition:all .3s ease}.reveal>.overlay.overlay-preview.loaded .viewport iframe{opacity:1;visibility:visible}.reveal>.overlay.overlay-preview.loaded .viewport-inner{position:absolute;z-index:-1;left:0;top:45%;width:100%;text-align:center;letter-spacing:normal}.reveal>.overlay.overlay-preview .x-frame-error{opacity:0;transition:opacity .3s ease .3s}.reveal>.overlay.overlay-preview.loaded .x-frame-error{opacity:1}.reveal>.overlay.overlay-preview.loaded .spinner{opacity:0;visibility:hidden;transform:scale(.2)}.reveal>.overlay.overlay-help .viewport{overflow:auto;color:#fff}.reveal>.overlay.overlay-help .viewport .viewport-inner{width:600px;margin:auto;padding:20px 20px 80px 20px;text-align:center;letter-spacing:normal}.reveal>.overlay.overlay-help .viewport .viewport-inner .title{font-size:20px}.reveal>.overlay.overlay-help .viewport .viewport-inner table{border:1px solid #fff;border-collapse:collapse;font-size:16px}.reveal>.overlay.overlay-help .viewport .viewport-inner table td,.reveal>.overlay.overlay-help .viewport .viewport-inner table th{width:200px;padding:14px;border:1px solid #fff;vertical-align:middle}.reveal>.overlay.overlay-help .viewport .viewport-inner table th{padding-top:20px;padding-bottom:20px}.reveal .playback{position:absolute;left:15px;bottom:20px;z-index:30;cursor:pointer;transition:all .4s ease;-webkit-tap-highlight-color:transparent}.reveal.overview .playback{opacity:0;visibility:hidden}.reveal .hljs{min-height:100%}.reveal .hljs table{margin:initial}.reveal .hljs-ln-code,.reveal .hljs-ln-numbers{padding:0;border:0}.reveal .hljs-ln-numbers{opacity:.6;padding-right:.75em;text-align:right;vertical-align:top}.reveal .hljs.has-highlights tr:not(.highlight-line){opacity:.4}.reveal .hljs:not(:first-child).fragment{position:absolute;top:0;left:0;width:100%;box-sizing:border-box}.reveal pre[data-auto-animate-target]{overflow:hidden}.reveal pre[data-auto-animate-target] code{height:100%}.reveal .roll{display:inline-block;line-height:1.2;overflow:hidden;vertical-align:top;perspective:400px;perspective-origin:50% 50%}.reveal .roll:hover{background:0 0;text-shadow:none}.reveal .roll span{display:block;position:relative;padding:0 2px;pointer-events:none;transition:all .4s ease;transform-origin:50% 0;transform-style:preserve-3d;-webkit-backface-visibility:hidden;backface-visibility:hidden}.reveal .roll:hover span{background:rgba(0,0,0,.5);transform:translate3d(0,0,-45px) rotateX(90deg)}.reveal .roll span:after{content:attr(data-title);display:block;position:absolute;left:0;top:0;padding:0 2px;-webkit-backface-visibility:hidden;backface-visibility:hidden;transform-origin:50% 0;transform:translate3d(0,110%,0) rotateX(-90deg)}.reveal aside.notes{display:none}.reveal .speaker-notes{display:none;position:absolute;width:33.3333333333%;height:100%;top:0;left:100%;padding:14px 18px 14px 18px;z-index:1;font-size:18px;line-height:1.4;border:1px solid rgba(0,0,0,.05);color:#222;background-color:#f5f5f5;overflow:auto;box-sizing:border-box;text-align:left;font-family:Helvetica,sans-serif;-webkit-overflow-scrolling:touch}.reveal .speaker-notes .notes-placeholder{color:#ccc;font-style:italic}.reveal .speaker-notes:focus{outline:0}.reveal .speaker-notes:before{content:"Speaker notes";display:block;margin-bottom:10px;opacity:.5}.reveal.show-notes{max-width:75%;overflow:visible}.reveal.show-notes .speaker-notes{display:block}@media screen and (min-width:1600px){.reveal .speaker-notes{font-size:20px}}@media screen and (max-width:1024px){.reveal.show-notes{border-left:0;max-width:none;max-height:70%;max-height:70vh;overflow:visible}.reveal.show-notes .speaker-notes{top:100%;left:0;width:100%;height:30vh;border:0}}@media screen and (max-width:600px){.reveal.show-notes{max-height:60%;max-height:60vh}.reveal.show-notes .speaker-notes{top:100%;height:40vh}.reveal .speaker-notes{font-size:14px}}.zoomed .reveal *,.zoomed .reveal :after,.zoomed .reveal :before{-webkit-backface-visibility:visible!important;backface-visibility:visible!important}.zoomed .reveal .controls,.zoomed .reveal .progress{opacity:0}.zoomed .reveal .roll span{background:0 0}.zoomed .reveal .roll span:after{visibility:hidden}html.print-pdf *{-webkit-print-color-adjust:exact}html.print-pdf{width:100%;height:100%;overflow:visible}html.print-pdf body{margin:0 auto!important;border:0;padding:0;float:none!important;overflow:visible}html.print-pdf .nestedarrow,html.print-pdf .reveal .controls,html.print-pdf .reveal .playback,html.print-pdf .reveal .progress,html.print-pdf .reveal.overview,html.print-pdf .state-background{display:none!important}html.print-pdf .reveal pre code{overflow:hidden!important;font-family:Courier,"Courier New",monospace!important}html.print-pdf .reveal{width:auto!important;height:auto!important;overflow:hidden!important}html.print-pdf .reveal .slides{position:static;width:100%!important;height:auto!important;zoom:1!important;pointer-events:initial;left:auto;top:auto;margin:0!important;padding:0!important;overflow:visible;display:block;perspective:none;perspective-origin:50% 50%}html.print-pdf .reveal .slides .pdf-page{position:relative;overflow:hidden;z-index:1;page-break-after:always}html.print-pdf .reveal .slides section{visibility:visible!important;display:block!important;position:absolute!important;margin:0!important;padding:0!important;box-sizing:border-box!important;min-height:1px;opacity:1!important;transform-style:flat!important;transform:none!important}html.print-pdf .reveal section.stack{position:relative!important;margin:0!important;padding:0!important;page-break-after:avoid!important;height:auto!important;min-height:auto!important}html.print-pdf .reveal img{box-shadow:none}html.print-pdf .reveal .backgrounds{display:none}html.print-pdf .reveal .slide-background{display:block!important;position:absolute;top:0;left:0;width:100%;height:100%;z-index:auto!important}html.print-pdf .reveal.show-notes{max-width:none;max-height:none}html.print-pdf .reveal .speaker-notes-pdf{display:block;width:100%;height:auto;max-height:none;top:auto;right:auto;bottom:auto;left:auto;z-index:100}html.print-pdf .reveal .speaker-notes-pdf[data-layout=separate-page]{position:relative;color:inherit;background-color:transparent;padding:20px;page-break-after:always;border:0}html.print-pdf .reveal .slide-number-pdf{display:block;position:absolute;font-size:14px}html.print-pdf .aria-status{display:none}@media print{html:not(.print-pdf){background:#fff;width:auto;height:auto;overflow:visible}html:not(.print-pdf) body{background:#fff;font-size:20pt;width:auto;height:auto;border:0;margin:0 5%;padding:0;overflow:visible;float:none!important}html:not(.print-pdf) .controls,html:not(.print-pdf) .fork-reveal,html:not(.print-pdf) .nestedarrow,html:not(.print-pdf) .reveal .backgrounds,html:not(.print-pdf) .reveal .progress,html:not(.print-pdf) .reveal .slide-number,html:not(.print-pdf) .share-reveal,html:not(.print-pdf) .state-background{display:none!important}html:not(.print-pdf) body,html:not(.print-pdf) li,html:not(.print-pdf) p,html:not(.print-pdf) td{font-size:20pt!important;color:#000}html:not(.print-pdf) h1,html:not(.print-pdf) h2,html:not(.print-pdf) h3,html:not(.print-pdf) h4,html:not(.print-pdf) h5,html:not(.print-pdf) h6{color:#000!important;height:auto;line-height:normal;text-align:left;letter-spacing:normal}html:not(.print-pdf) h1{font-size:28pt!important}html:not(.print-pdf) h2{font-size:24pt!important}html:not(.print-pdf) h3{font-size:22pt!important}html:not(.print-pdf) h4{font-size:22pt!important;font-variant:small-caps}html:not(.print-pdf) h5{font-size:21pt!important}html:not(.print-pdf) h6{font-size:20pt!important;font-style:italic}html:not(.print-pdf) a:link,html:not(.print-pdf) a:visited{color:#000!important;font-weight:700;text-decoration:underline}html:not(.print-pdf) div,html:not(.print-pdf) ol,html:not(.print-pdf) p,html:not(.print-pdf) ul{visibility:visible;position:static;width:auto;height:auto;display:block;overflow:visible;margin:0;text-align:left!important}html:not(.print-pdf) .reveal pre,html:not(.print-pdf) .reveal table{margin-left:0;margin-right:0}html:not(.print-pdf) .reveal pre code{padding:20px}html:not(.print-pdf) .reveal blockquote{margin:20px 0}html:not(.print-pdf) .reveal .slides{position:static!important;width:auto!important;height:auto!important;left:0!important;top:0!important;margin-left:0!important;margin-top:0!important;padding:0!important;zoom:1!important;transform:none!important;overflow:visible!important;display:block!important;text-align:left!important;perspective:none;perspective-origin:50% 50%}html:not(.print-pdf) .reveal .slides section{visibility:visible!important;position:static!important;width:auto!important;height:auto!important;display:block!important;overflow:visible!important;left:0!important;top:0!important;margin-left:0!important;margin-top:0!important;padding:60px 20px!important;z-index:auto!important;opacity:1!important;page-break-after:always!important;transform-style:flat!important;transform:none!important;transition:none!important}html:not(.print-pdf) .reveal .slides section.stack{padding:0!important}html:not(.print-pdf) .reveal section:last-of-type{page-break-after:avoid!important}html:not(.print-pdf) .reveal section .fragment{opacity:1!important;visibility:visible!important;transform:none!important}html:not(.print-pdf) .reveal .r-fit-text{white-space:normal!important}html:not(.print-pdf) .reveal section img{display:block;margin:15px 0;background:#fff;border:1px solid #666;box-shadow:none}html:not(.print-pdf) .reveal section small{font-size:.8em}html:not(.print-pdf) .reveal .hljs{max-height:100%;white-space:pre-wrap;word-wrap:break-word;word-break:break-word;font-size:15pt}html:not(.print-pdf) .reveal .hljs .hljs-ln-numbers{white-space:nowrap}html:not(.print-pdf) .reveal .hljs td{font-size:inherit!important;color:inherit!important}} \ No newline at end of file diff --git a/mstreams/presentation/dist/reveal.esm.js b/mstreams/presentation/dist/reveal.esm.js new file mode 100644 index 0000000..b30e429 --- /dev/null +++ b/mstreams/presentation/dist/reveal.esm.js @@ -0,0 +1,9 @@ +/*! +* reveal.js 4.3.1 +* https://revealjs.com +* MIT licensed +* +* Copyright (C) 2011-2022 Hakim El Hattab, https://hakim.se +*/ +const e=(e,t)=>{for(let i in t)e[i]=t[i];return e},t=(e,t)=>Array.from(e.querySelectorAll(t)),i=(e,t,i)=>{i?e.classList.add(t):e.classList.remove(t)},a=e=>{if("string"==typeof e){if("null"===e)return null;if("true"===e)return!0;if("false"===e)return!1;if(e.match(/^-?[\d\.]+$/))return parseFloat(e)}return e},s=(e,t)=>{e.style.transform=t},n=(e,t)=>{let i=e.matches||e.matchesSelector||e.msMatchesSelector;return!(!i||!i.call(e,t))},r=(e,t)=>{if("function"==typeof e.closest)return e.closest(t);for(;e;){if(n(e,t))return e;e=e.parentNode}return null},o=(e,t,i,a="")=>{let s=e.querySelectorAll("."+i);for(let t=0;t{let t=document.createElement("style");return t.type="text/css",e&&e.length>0&&(t.styleSheet?t.styleSheet.cssText=e:t.appendChild(document.createTextNode(e))),document.head.appendChild(t),t},d=()=>{let e={};location.search.replace(/[A-Z0-9]+?=([\w\.%-]*)/gi,(t=>{e[t.split("=").shift()]=t.split("=").pop()}));for(let t in e){let i=e[t];e[t]=a(unescape(i))}return void 0!==e.dependencies&&delete e.dependencies,e},c=(e,t=0)=>{if(e){let i,a=e.style.height;return e.style.height="0px",e.parentNode.style.height="auto",i=t-e.parentNode.offsetHeight,e.style.height=a+"px",e.parentNode.style.removeProperty("height"),i}return t},h={mp4:"video/mp4",m4a:"video/mp4",ogv:"video/ogg",mpeg:"video/mpeg",webm:"video/webm"},u=navigator.userAgent,g=/(iphone|ipod|ipad|android)/gi.test(u)||"MacIntel"===navigator.platform&&navigator.maxTouchPoints>1;/chrome/i.test(u)&&/edge/i.test(u);const v=/android/gi.test(u);var p={};Object.defineProperty(p,"__esModule",{value:!0});var m=Object.assign||function(e){for(var t=1;t1&&void 0!==arguments[1]?arguments[1]:{};return"string"==typeof e?x(t(document.querySelectorAll(e)),i):x([e],i)[0]}}("undefined"==typeof window?null:window);class b{constructor(e){this.Reveal=e,this.startEmbeddedIframe=this.startEmbeddedIframe.bind(this)}shouldPreload(e){let t=this.Reveal.getConfig().preloadIframes;return"boolean"!=typeof t&&(t=e.hasAttribute("data-preload")),t}load(e,i={}){e.style.display=this.Reveal.getConfig().display,t(e,"img[data-src], video[data-src], audio[data-src], iframe[data-src]").forEach((e=>{("IFRAME"!==e.tagName||this.shouldPreload(e))&&(e.setAttribute("src",e.getAttribute("data-src")),e.setAttribute("data-lazy-loaded",""),e.removeAttribute("data-src"))})),t(e,"video, audio").forEach((e=>{let i=0;t(e,"source[data-src]").forEach((e=>{e.setAttribute("src",e.getAttribute("data-src")),e.removeAttribute("data-src"),e.setAttribute("data-lazy-loaded",""),i+=1})),g&&"VIDEO"===e.tagName&&e.setAttribute("playsinline",""),i>0&&e.load()}));let a=e.slideBackgroundElement;if(a){a.style.display="block";let t=e.slideBackgroundContentElement,s=e.getAttribute("data-background-iframe");if(!1===a.hasAttribute("data-loaded")){a.setAttribute("data-loaded","true");let n=e.getAttribute("data-background-image"),r=e.getAttribute("data-background-video"),o=e.hasAttribute("data-background-video-loop"),l=e.hasAttribute("data-background-video-muted");if(n)/^data:/.test(n.trim())?t.style.backgroundImage=`url(${n.trim()})`:t.style.backgroundImage=n.split(",").map((e=>`url(${encodeURI(e.trim())})`)).join(",");else if(r&&!this.Reveal.isSpeakerNotes()){let e=document.createElement("video");o&&e.setAttribute("loop",""),l&&(e.muted=!0),g&&(e.muted=!0,e.setAttribute("playsinline","")),r.split(",").forEach((t=>{let i=((e="")=>h[e.split(".").pop()])(t);e.innerHTML+=i?``:``})),t.appendChild(e)}else if(s&&!0!==i.excludeIframes){let e=document.createElement("iframe");e.setAttribute("allowfullscreen",""),e.setAttribute("mozallowfullscreen",""),e.setAttribute("webkitallowfullscreen",""),e.setAttribute("allow","autoplay"),e.setAttribute("data-src",s),e.style.width="100%",e.style.height="100%",e.style.maxHeight="100%",e.style.maxWidth="100%",t.appendChild(e)}}let n=t.querySelector("iframe[data-src]");n&&this.shouldPreload(a)&&!/autoplay=(1|true|yes)/gi.test(s)&&n.getAttribute("src")!==s&&n.setAttribute("src",s)}this.layout(e)}layout(e){Array.from(e.querySelectorAll(".r-fit-text")).forEach((e=>{f(e,{minSize:24,maxSize:.8*this.Reveal.getConfig().height,observeMutations:!1,observeWindow:!1})}))}unload(e){e.style.display="none";let i=this.Reveal.getSlideBackground(e);i&&(i.style.display="none",t(i,"iframe[src]").forEach((e=>{e.removeAttribute("src")}))),t(e,"video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]").forEach((e=>{e.setAttribute("data-src",e.getAttribute("src")),e.removeAttribute("src")})),t(e,"video[data-lazy-loaded] source[src], audio source[src]").forEach((e=>{e.setAttribute("data-src",e.getAttribute("src")),e.removeAttribute("src")}))}formatEmbeddedContent(){let e=(e,i,a)=>{t(this.Reveal.getSlidesElement(),"iframe["+e+'*="'+i+'"]').forEach((t=>{let i=t.getAttribute(e);i&&-1===i.indexOf(a)&&t.setAttribute(e,i+(/\?/.test(i)?"&":"?")+a)}))};e("src","youtube.com/embed/","enablejsapi=1"),e("data-src","youtube.com/embed/","enablejsapi=1"),e("src","player.vimeo.com/","api=1"),e("data-src","player.vimeo.com/","api=1")}startEmbeddedContent(e){e&&!this.Reveal.isSpeakerNotes()&&(t(e,'img[src$=".gif"]').forEach((e=>{e.setAttribute("src",e.getAttribute("src"))})),t(e,"video, audio").forEach((e=>{if(r(e,".fragment")&&!r(e,".fragment.visible"))return;let t=this.Reveal.getConfig().autoPlayMedia;if("boolean"!=typeof t&&(t=e.hasAttribute("data-autoplay")||!!r(e,".slide-background")),t&&"function"==typeof e.play)if(e.readyState>1)this.startEmbeddedMedia({target:e});else if(g){let t=e.play();t&&"function"==typeof t.catch&&!1===e.controls&&t.catch((()=>{e.controls=!0,e.addEventListener("play",(()=>{e.controls=!1}))}))}else e.removeEventListener("loadeddata",this.startEmbeddedMedia),e.addEventListener("loadeddata",this.startEmbeddedMedia)})),t(e,"iframe[src]").forEach((e=>{r(e,".fragment")&&!r(e,".fragment.visible")||this.startEmbeddedIframe({target:e})})),t(e,"iframe[data-src]").forEach((e=>{r(e,".fragment")&&!r(e,".fragment.visible")||e.getAttribute("src")!==e.getAttribute("data-src")&&(e.removeEventListener("load",this.startEmbeddedIframe),e.addEventListener("load",this.startEmbeddedIframe),e.setAttribute("src",e.getAttribute("data-src")))})))}startEmbeddedMedia(e){let t=!!r(e.target,"html"),i=!!r(e.target,".present");t&&i&&(e.target.currentTime=0,e.target.play()),e.target.removeEventListener("loadeddata",this.startEmbeddedMedia)}startEmbeddedIframe(e){let t=e.target;if(t&&t.contentWindow){let i=!!r(e.target,"html"),a=!!r(e.target,".present");if(i&&a){let e=this.Reveal.getConfig().autoPlayMedia;"boolean"!=typeof e&&(e=t.hasAttribute("data-autoplay")||!!r(t,".slide-background")),/youtube\.com\/embed\//.test(t.getAttribute("src"))&&e?t.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}',"*"):/player\.vimeo\.com\//.test(t.getAttribute("src"))&&e?t.contentWindow.postMessage('{"method":"play"}',"*"):t.contentWindow.postMessage("slide:start","*")}}}stopEmbeddedContent(i,a={}){a=e({unloadIframes:!0},a),i&&i.parentNode&&(t(i,"video, audio").forEach((e=>{e.hasAttribute("data-ignore")||"function"!=typeof e.pause||(e.setAttribute("data-paused-by-reveal",""),e.pause())})),t(i,"iframe").forEach((e=>{e.contentWindow&&e.contentWindow.postMessage("slide:stop","*"),e.removeEventListener("load",this.startEmbeddedIframe)})),t(i,'iframe[src*="youtube.com/embed/"]').forEach((e=>{!e.hasAttribute("data-ignore")&&e.contentWindow&&"function"==typeof e.contentWindow.postMessage&&e.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}',"*")})),t(i,'iframe[src*="player.vimeo.com/"]').forEach((e=>{!e.hasAttribute("data-ignore")&&e.contentWindow&&"function"==typeof e.contentWindow.postMessage&&e.contentWindow.postMessage('{"method":"pause"}',"*")})),!0===a.unloadIframes&&t(i,"iframe[data-src]").forEach((e=>{e.setAttribute("src","about:blank"),e.removeAttribute("src")})))}}class y{constructor(e){this.Reveal=e}render(){this.element=document.createElement("div"),this.element.className="slide-number",this.Reveal.getRevealElement().appendChild(this.element)}configure(e,t){let i="none";e.slideNumber&&!this.Reveal.isPrintingPDF()&&("all"===e.showSlideNumber||"speaker"===e.showSlideNumber&&this.Reveal.isSpeakerNotes())&&(i="block"),this.element.style.display=i}update(){this.Reveal.getConfig().slideNumber&&this.element&&(this.element.innerHTML=this.getSlideNumber())}getSlideNumber(e=this.Reveal.getCurrentSlide()){let t,i=this.Reveal.getConfig(),a="h.v";if("function"==typeof i.slideNumber)t=i.slideNumber(e);else{"string"==typeof i.slideNumber&&(a=i.slideNumber),/c/.test(a)||1!==this.Reveal.getHorizontalSlides().length||(a="c");let s=e&&"uncounted"===e.dataset.visibility?0:1;switch(t=[],a){case"c":t.push(this.Reveal.getSlidePastCount(e)+s);break;case"c/t":t.push(this.Reveal.getSlidePastCount(e)+s,"/",this.Reveal.getTotalSlides());break;default:let i=this.Reveal.getIndices(e);t.push(i.h+s);let n="h/v"===a?"/":".";this.Reveal.isVerticalSlide(e)&&t.push(n,i.v+1)}}let s="#"+this.Reveal.location.getHash(e);return this.formatNumber(t[0],t[1],t[2],s)}formatNumber(e,t,i,a="#"+this.Reveal.location.getHash()){return"number"!=typeof i||isNaN(i)?`\n\t\t\t\t\t${e}\n\t\t\t\t\t`:`\n\t\t\t\t\t${e}\n\t\t\t\t\t${t}\n\t\t\t\t\t${i}\n\t\t\t\t\t`}destroy(){this.element.remove()}}const w=e=>{let t=e.match(/^#([0-9a-f]{3})$/i);if(t&&t[1])return t=t[1],{r:17*parseInt(t.charAt(0),16),g:17*parseInt(t.charAt(1),16),b:17*parseInt(t.charAt(2),16)};let i=e.match(/^#([0-9a-f]{6})$/i);if(i&&i[1])return i=i[1],{r:parseInt(i.slice(0,2),16),g:parseInt(i.slice(2,4),16),b:parseInt(i.slice(4,6),16)};let a=e.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);if(a)return{r:parseInt(a[1],10),g:parseInt(a[2],10),b:parseInt(a[3],10)};let s=e.match(/^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i);return s?{r:parseInt(s[1],10),g:parseInt(s[2],10),b:parseInt(s[3],10),a:parseFloat(s[4])}:null};class E{constructor(e){this.Reveal=e}render(){this.element=document.createElement("div"),this.element.className="backgrounds",this.Reveal.getRevealElement().appendChild(this.element)}create(){this.element.innerHTML="",this.element.classList.add("no-transition"),this.Reveal.getHorizontalSlides().forEach((e=>{let i=this.createBackground(e,this.element);t(e,"section").forEach((e=>{this.createBackground(e,i),i.classList.add("stack")}))})),this.Reveal.getConfig().parallaxBackgroundImage?(this.element.style.backgroundImage='url("'+this.Reveal.getConfig().parallaxBackgroundImage+'")',this.element.style.backgroundSize=this.Reveal.getConfig().parallaxBackgroundSize,this.element.style.backgroundRepeat=this.Reveal.getConfig().parallaxBackgroundRepeat,this.element.style.backgroundPosition=this.Reveal.getConfig().parallaxBackgroundPosition,setTimeout((()=>{this.Reveal.getRevealElement().classList.add("has-parallax-background")}),1)):(this.element.style.backgroundImage="",this.Reveal.getRevealElement().classList.remove("has-parallax-background"))}createBackground(e,t){let i=document.createElement("div");i.className="slide-background "+e.className.replace(/present|past|future/,"");let a=document.createElement("div");return a.className="slide-background-content",i.appendChild(a),t.appendChild(i),e.slideBackgroundElement=i,e.slideBackgroundContentElement=a,this.sync(e),i}sync(e){const t=e.slideBackgroundElement,i=e.slideBackgroundContentElement,a={background:e.getAttribute("data-background"),backgroundSize:e.getAttribute("data-background-size"),backgroundImage:e.getAttribute("data-background-image"),backgroundVideo:e.getAttribute("data-background-video"),backgroundIframe:e.getAttribute("data-background-iframe"),backgroundColor:e.getAttribute("data-background-color"),backgroundGradient:e.getAttribute("data-background-gradient"),backgroundRepeat:e.getAttribute("data-background-repeat"),backgroundPosition:e.getAttribute("data-background-position"),backgroundTransition:e.getAttribute("data-background-transition"),backgroundOpacity:e.getAttribute("data-background-opacity")},s=e.hasAttribute("data-preload");e.classList.remove("has-dark-background"),e.classList.remove("has-light-background"),t.removeAttribute("data-loaded"),t.removeAttribute("data-background-hash"),t.removeAttribute("data-background-size"),t.removeAttribute("data-background-transition"),t.style.backgroundColor="",i.style.backgroundSize="",i.style.backgroundRepeat="",i.style.backgroundPosition="",i.style.backgroundImage="",i.style.opacity="",i.innerHTML="",a.background&&(/^(http|file|\/\/)/gi.test(a.background)||/\.(svg|png|jpg|jpeg|gif|bmp|webp)([?#\s]|$)/gi.test(a.background)?e.setAttribute("data-background-image",a.background):t.style.background=a.background),(a.background||a.backgroundColor||a.backgroundGradient||a.backgroundImage||a.backgroundVideo||a.backgroundIframe)&&t.setAttribute("data-background-hash",a.background+a.backgroundSize+a.backgroundImage+a.backgroundVideo+a.backgroundIframe+a.backgroundColor+a.backgroundGradient+a.backgroundRepeat+a.backgroundPosition+a.backgroundTransition+a.backgroundOpacity),a.backgroundSize&&t.setAttribute("data-background-size",a.backgroundSize),a.backgroundColor&&(t.style.backgroundColor=a.backgroundColor),a.backgroundGradient&&(t.style.backgroundImage=a.backgroundGradient),a.backgroundTransition&&t.setAttribute("data-background-transition",a.backgroundTransition),s&&t.setAttribute("data-preload",""),a.backgroundSize&&(i.style.backgroundSize=a.backgroundSize),a.backgroundRepeat&&(i.style.backgroundRepeat=a.backgroundRepeat),a.backgroundPosition&&(i.style.backgroundPosition=a.backgroundPosition),a.backgroundOpacity&&(i.style.opacity=a.backgroundOpacity);let n=a.backgroundColor;if(!n||!w(n)){let e=window.getComputedStyle(t);e&&e.backgroundColor&&(n=e.backgroundColor)}if(n){const t=w(n);t&&0!==t.a&&("string"==typeof(r=n)&&(r=w(r)),(r?(299*r.r+587*r.g+114*r.b)/1e3:null)<128?e.classList.add("has-dark-background"):e.classList.add("has-light-background"))}var r}update(e=!1){let i=this.Reveal.getCurrentSlide(),a=this.Reveal.getIndices(),s=null,n=this.Reveal.getConfig().rtl?"future":"past",r=this.Reveal.getConfig().rtl?"past":"future";if(Array.from(this.element.childNodes).forEach(((i,o)=>{i.classList.remove("past","present","future"),oa.h?i.classList.add(r):(i.classList.add("present"),s=i),(e||o===a.h)&&t(i,".slide-background").forEach(((e,t)=>{e.classList.remove("past","present","future"),ta.v?e.classList.add("future"):(e.classList.add("present"),o===a.h&&(s=e))}))})),this.previousBackground&&this.Reveal.slideContent.stopEmbeddedContent(this.previousBackground,{unloadIframes:!this.Reveal.slideContent.shouldPreload(this.previousBackground)}),s){this.Reveal.slideContent.startEmbeddedContent(s);let e=s.querySelector(".slide-background-content");if(e){let t=e.style.backgroundImage||"";/\.gif/i.test(t)&&(e.style.backgroundImage="",window.getComputedStyle(e).opacity,e.style.backgroundImage=t)}let t=this.previousBackground?this.previousBackground.getAttribute("data-background-hash"):null,i=s.getAttribute("data-background-hash");i&&i===t&&s!==this.previousBackground&&this.element.classList.add("no-transition"),this.previousBackground=s}i&&["has-light-background","has-dark-background"].forEach((e=>{i.classList.contains(e)?this.Reveal.getRevealElement().classList.add(e):this.Reveal.getRevealElement().classList.remove(e)}),this),setTimeout((()=>{this.element.classList.remove("no-transition")}),1)}updateParallax(){let e=this.Reveal.getIndices();if(this.Reveal.getConfig().parallaxBackgroundImage){let t,i,a=this.Reveal.getHorizontalSlides(),s=this.Reveal.getVerticalSlides(),n=this.element.style.backgroundSize.split(" ");1===n.length?t=i=parseInt(n[0],10):(t=parseInt(n[0],10),i=parseInt(n[1],10));let r,o,l=this.element.offsetWidth,d=a.length;r="number"==typeof this.Reveal.getConfig().parallaxBackgroundHorizontal?this.Reveal.getConfig().parallaxBackgroundHorizontal:d>1?(t-l)/(d-1):0,o=r*e.h*-1;let c,h,u=this.element.offsetHeight,g=s.length;c="number"==typeof this.Reveal.getConfig().parallaxBackgroundVertical?this.Reveal.getConfig().parallaxBackgroundVertical:(i-u)/(g-1),h=g>0?c*e.v:0,this.element.style.backgroundPosition=o+"px "+-h+"px"}}destroy(){this.element.remove()}}const R=/registerPlugin|registerKeyboardShortcut|addKeyBinding|addEventListener|showPreview/,S=/fade-(down|up|right|left|out|in-then-out|in-then-semi-out)|semi-fade-out|current-visible|shrink|grow/;let A=0;class k{constructor(e){this.Reveal=e}run(e,t){this.reset();let i=this.Reveal.getSlides(),a=i.indexOf(t),s=i.indexOf(e);if(e.hasAttribute("data-auto-animate")&&t.hasAttribute("data-auto-animate")&&e.getAttribute("data-auto-animate-id")===t.getAttribute("data-auto-animate-id")&&!(a>s?t:e).hasAttribute("data-auto-animate-restart")){this.autoAnimateStyleSheet=this.autoAnimateStyleSheet||l();let i=this.getAutoAnimateOptions(t);e.dataset.autoAnimate="pending",t.dataset.autoAnimate="pending",i.slideDirection=a>s?"forward":"backward";let n="none"===e.style.display;n&&(e.style.display=this.Reveal.getConfig().display);let r=this.getAutoAnimatableElements(e,t).map((e=>this.autoAnimateElements(e.from,e.to,e.options||{},i,A++)));if(n&&(e.style.display="none"),"false"!==t.dataset.autoAnimateUnmatched&&!0===this.Reveal.getConfig().autoAnimateUnmatched){let e=.8*i.duration,a=.2*i.duration;this.getUnmatchedAutoAnimateElements(t).forEach((e=>{let t=this.getAutoAnimateOptions(e,i),a="unmatched";t.duration===i.duration&&t.delay===i.delay||(a="unmatched-"+A++,r.push(`[data-auto-animate="running"] [data-auto-animate-target="${a}"] { transition: opacity ${t.duration}s ease ${t.delay}s; }`)),e.dataset.autoAnimateTarget=a}),this),r.push(`[data-auto-animate="running"] [data-auto-animate-target="unmatched"] { transition: opacity ${e}s ease ${a}s; }`)}this.autoAnimateStyleSheet.innerHTML=r.join(""),requestAnimationFrame((()=>{this.autoAnimateStyleSheet&&(getComputedStyle(this.autoAnimateStyleSheet).fontWeight,t.dataset.autoAnimate="running")})),this.Reveal.dispatchEvent({type:"autoanimate",data:{fromSlide:e,toSlide:t,sheet:this.autoAnimateStyleSheet}})}}reset(){t(this.Reveal.getRevealElement(),'[data-auto-animate]:not([data-auto-animate=""])').forEach((e=>{e.dataset.autoAnimate=""})),t(this.Reveal.getRevealElement(),"[data-auto-animate-target]").forEach((e=>{delete e.dataset.autoAnimateTarget})),this.autoAnimateStyleSheet&&this.autoAnimateStyleSheet.parentNode&&(this.autoAnimateStyleSheet.parentNode.removeChild(this.autoAnimateStyleSheet),this.autoAnimateStyleSheet=null)}autoAnimateElements(e,t,i,a,s){e.dataset.autoAnimateTarget="",t.dataset.autoAnimateTarget=s;let n=this.getAutoAnimateOptions(t,a);void 0!==i.delay&&(n.delay=i.delay),void 0!==i.duration&&(n.duration=i.duration),void 0!==i.easing&&(n.easing=i.easing);let r=this.getAutoAnimatableProperties("from",e,i),o=this.getAutoAnimatableProperties("to",t,i);if(t.classList.contains("fragment")&&(delete o.styles.opacity,e.classList.contains("fragment"))){(e.className.match(S)||[""])[0]===(t.className.match(S)||[""])[0]&&"forward"===a.slideDirection&&t.classList.add("visible","disabled")}if(!1!==i.translate||!1!==i.scale){let e=this.Reveal.getScale(),t={x:(r.x-o.x)/e,y:(r.y-o.y)/e,scaleX:r.width/o.width,scaleY:r.height/o.height};t.x=Math.round(1e3*t.x)/1e3,t.y=Math.round(1e3*t.y)/1e3,t.scaleX=Math.round(1e3*t.scaleX)/1e3,t.scaleX=Math.round(1e3*t.scaleX)/1e3;let a=!1!==i.translate&&(0!==t.x||0!==t.y),s=!1!==i.scale&&(0!==t.scaleX||0!==t.scaleY);if(a||s){let e=[];a&&e.push(`translate(${t.x}px, ${t.y}px)`),s&&e.push(`scale(${t.scaleX}, ${t.scaleY})`),r.styles.transform=e.join(" "),r.styles["transform-origin"]="top left",o.styles.transform="none"}}for(let e in o.styles){const t=o.styles[e],i=r.styles[e];t===i?delete o.styles[e]:(!0===t.explicitValue&&(o.styles[e]=t.value),!0===i.explicitValue&&(r.styles[e]=i.value))}let l="",d=Object.keys(o.styles);if(d.length>0){r.styles.transition="none",o.styles.transition=`all ${n.duration}s ${n.easing} ${n.delay}s`,o.styles["transition-property"]=d.join(", "),o.styles["will-change"]=d.join(", "),l='[data-auto-animate-target="'+s+'"] {'+Object.keys(r.styles).map((e=>e+": "+r.styles[e]+" !important;")).join("")+'}[data-auto-animate="running"] [data-auto-animate-target="'+s+'"] {'+Object.keys(o.styles).map((e=>e+": "+o.styles[e]+" !important;")).join("")+"}"}return l}getAutoAnimateOptions(t,i){let a={easing:this.Reveal.getConfig().autoAnimateEasing,duration:this.Reveal.getConfig().autoAnimateDuration,delay:0};if(a=e(a,i),t.parentNode){let e=r(t.parentNode,"[data-auto-animate-target]");e&&(a=this.getAutoAnimateOptions(e,a))}return t.dataset.autoAnimateEasing&&(a.easing=t.dataset.autoAnimateEasing),t.dataset.autoAnimateDuration&&(a.duration=parseFloat(t.dataset.autoAnimateDuration)),t.dataset.autoAnimateDelay&&(a.delay=parseFloat(t.dataset.autoAnimateDelay)),a}getAutoAnimatableProperties(e,t,i){let a=this.Reveal.getConfig(),s={styles:[]};if(!1!==i.translate||!1!==i.scale){let e;if("function"==typeof i.measure)e=i.measure(t);else if(a.center)e=t.getBoundingClientRect();else{let i=this.Reveal.getScale();e={x:t.offsetLeft*i,y:t.offsetTop*i,width:t.offsetWidth*i,height:t.offsetHeight*i}}s.x=e.x,s.y=e.y,s.width=e.width,s.height=e.height}const n=getComputedStyle(t);return(i.styles||a.autoAnimateStyles).forEach((t=>{let i;"string"==typeof t&&(t={property:t}),i=void 0!==t.from&&"from"===e?{value:t.from,explicitValue:!0}:void 0!==t.to&&"to"===e?{value:t.to,explicitValue:!0}:n[t.property],""!==i&&(s.styles[t.property]=i)})),s}getAutoAnimatableElements(e,t){let i=("function"==typeof this.Reveal.getConfig().autoAnimateMatcher?this.Reveal.getConfig().autoAnimateMatcher:this.getAutoAnimatePairs).call(this,e,t),a=[];return i.filter(((e,t)=>{if(-1===a.indexOf(e.to))return a.push(e.to),!0}))}getAutoAnimatePairs(e,t){let i=[];const a="h1, h2, h3, h4, h5, h6, p, li";return this.findAutoAnimateMatches(i,e,t,"[data-id]",(e=>e.nodeName+":::"+e.getAttribute("data-id"))),this.findAutoAnimateMatches(i,e,t,a,(e=>e.nodeName+":::"+e.innerText)),this.findAutoAnimateMatches(i,e,t,"img, video, iframe",(e=>e.nodeName+":::"+(e.getAttribute("src")||e.getAttribute("data-src")))),this.findAutoAnimateMatches(i,e,t,"pre",(e=>e.nodeName+":::"+e.innerText)),i.forEach((e=>{n(e.from,a)?e.options={scale:!1}:n(e.from,"pre")&&(e.options={scale:!1,styles:["width","height"]},this.findAutoAnimateMatches(i,e.from,e.to,".hljs .hljs-ln-code",(e=>e.textContent),{scale:!1,styles:[],measure:this.getLocalBoundingBox.bind(this)}),this.findAutoAnimateMatches(i,e.from,e.to,".hljs .hljs-ln-line[data-line-number]",(e=>e.getAttribute("data-line-number")),{scale:!1,styles:["width"],measure:this.getLocalBoundingBox.bind(this)}))}),this),i}getLocalBoundingBox(e){const t=this.Reveal.getScale();return{x:Math.round(e.offsetLeft*t*100)/100,y:Math.round(e.offsetTop*t*100)/100,width:Math.round(e.offsetWidth*t*100)/100,height:Math.round(e.offsetHeight*t*100)/100}}findAutoAnimateMatches(e,t,i,a,s,n){let r={},o={};[].slice.call(t.querySelectorAll(a)).forEach(((e,t)=>{const i=s(e);"string"==typeof i&&i.length&&(r[i]=r[i]||[],r[i].push(e))})),[].slice.call(i.querySelectorAll(a)).forEach(((t,i)=>{const a=s(t);let l;if(o[a]=o[a]||[],o[a].push(t),r[a]){const e=o[a].length-1,t=r[a].length-1;r[a][e]?(l=r[a][e],r[a][e]=null):r[a][t]&&(l=r[a][t],r[a][t]=null)}l&&e.push({from:l,to:t,options:n})}))}getUnmatchedAutoAnimateElements(e){return[].slice.call(e.children).reduce(((e,t)=>{const i=t.querySelector("[data-auto-animate-target]");return t.hasAttribute("data-auto-animate-target")||i||e.push(t),t.querySelector("[data-auto-animate-target]")&&(e=e.concat(this.getUnmatchedAutoAnimateElements(t))),e}),[])}}class L{constructor(e){this.Reveal=e}configure(e,t){!1===e.fragments?this.disable():!1===t.fragments&&this.enable()}disable(){t(this.Reveal.getSlidesElement(),".fragment").forEach((e=>{e.classList.add("visible"),e.classList.remove("current-fragment")}))}enable(){t(this.Reveal.getSlidesElement(),".fragment").forEach((e=>{e.classList.remove("visible"),e.classList.remove("current-fragment")}))}availableRoutes(){let e=this.Reveal.getCurrentSlide();if(e&&this.Reveal.getConfig().fragments){let t=e.querySelectorAll(".fragment:not(.disabled)"),i=e.querySelectorAll(".fragment:not(.disabled):not(.visible)");return{prev:t.length-i.length>0,next:!!i.length}}return{prev:!1,next:!1}}sort(e,t=!1){e=Array.from(e);let i=[],a=[],s=[];e.forEach((e=>{if(e.hasAttribute("data-fragment-index")){let t=parseInt(e.getAttribute("data-fragment-index"),10);i[t]||(i[t]=[]),i[t].push(e)}else a.push([e])})),i=i.concat(a);let n=0;return i.forEach((e=>{e.forEach((e=>{s.push(e),e.setAttribute("data-fragment-index",n)})),n++})),!0===t?i:s}sortAll(){this.Reveal.getHorizontalSlides().forEach((e=>{let i=t(e,"section");i.forEach(((e,t)=>{this.sort(e.querySelectorAll(".fragment"))}),this),0===i.length&&this.sort(e.querySelectorAll(".fragment"))}))}update(e,t){let i={shown:[],hidden:[]},a=this.Reveal.getCurrentSlide();if(a&&this.Reveal.getConfig().fragments&&(t=t||this.sort(a.querySelectorAll(".fragment"))).length){let s=0;if("number"!=typeof e){let t=this.sort(a.querySelectorAll(".fragment.visible")).pop();t&&(e=parseInt(t.getAttribute("data-fragment-index")||0,10))}Array.from(t).forEach(((t,a)=>{if(t.hasAttribute("data-fragment-index")&&(a=parseInt(t.getAttribute("data-fragment-index"),10)),s=Math.max(s,a),a<=e){let s=t.classList.contains("visible");t.classList.add("visible"),t.classList.remove("current-fragment"),a===e&&(this.Reveal.announceStatus(this.Reveal.getStatusText(t)),t.classList.add("current-fragment"),this.Reveal.slideContent.startEmbeddedContent(t)),s||(i.shown.push(t),this.Reveal.dispatchEvent({target:t,type:"visible",bubbles:!1}))}else{let e=t.classList.contains("visible");t.classList.remove("visible"),t.classList.remove("current-fragment"),e&&(this.Reveal.slideContent.stopEmbeddedContent(t),i.hidden.push(t),this.Reveal.dispatchEvent({target:t,type:"hidden",bubbles:!1}))}})),e="number"==typeof e?e:-1,e=Math.max(Math.min(e,s),-1),a.setAttribute("data-fragment",e)}return i}sync(e=this.Reveal.getCurrentSlide()){return this.sort(e.querySelectorAll(".fragment"))}goto(e,t=0){let i=this.Reveal.getCurrentSlide();if(i&&this.Reveal.getConfig().fragments){let a=this.sort(i.querySelectorAll(".fragment:not(.disabled)"));if(a.length){if("number"!=typeof e){let t=this.sort(i.querySelectorAll(".fragment:not(.disabled).visible")).pop();e=t?parseInt(t.getAttribute("data-fragment-index")||0,10):-1}e+=t;let s=this.update(e,a);return s.hidden.length&&this.Reveal.dispatchEvent({type:"fragmenthidden",data:{fragment:s.hidden[0],fragments:s.hidden}}),s.shown.length&&this.Reveal.dispatchEvent({type:"fragmentshown",data:{fragment:s.shown[0],fragments:s.shown}}),this.Reveal.controls.update(),this.Reveal.progress.update(),this.Reveal.getConfig().fragmentInURL&&this.Reveal.location.writeURL(),!(!s.shown.length&&!s.hidden.length)}}return!1}next(){return this.goto(null,1)}prev(){return this.goto(null,-1)}}class C{constructor(e){this.Reveal=e,this.active=!1,this.onSlideClicked=this.onSlideClicked.bind(this)}activate(){if(this.Reveal.getConfig().overview&&!this.isActive()){this.active=!0,this.Reveal.getRevealElement().classList.add("overview"),this.Reveal.cancelAutoSlide(),this.Reveal.getSlidesElement().appendChild(this.Reveal.getBackgroundsElement()),t(this.Reveal.getRevealElement(),".slides section").forEach((e=>{e.classList.contains("stack")||e.addEventListener("click",this.onSlideClicked,!0)}));const e=70,i=this.Reveal.getComputedSlideSize();this.overviewSlideWidth=i.width+e,this.overviewSlideHeight=i.height+e,this.Reveal.getConfig().rtl&&(this.overviewSlideWidth=-this.overviewSlideWidth),this.Reveal.updateSlidesVisibility(),this.layout(),this.update(),this.Reveal.layout();const a=this.Reveal.getIndices();this.Reveal.dispatchEvent({type:"overviewshown",data:{indexh:a.h,indexv:a.v,currentSlide:this.Reveal.getCurrentSlide()}})}}layout(){this.Reveal.getHorizontalSlides().forEach(((e,i)=>{e.setAttribute("data-index-h",i),s(e,"translate3d("+i*this.overviewSlideWidth+"px, 0, 0)"),e.classList.contains("stack")&&t(e,"section").forEach(((e,t)=>{e.setAttribute("data-index-h",i),e.setAttribute("data-index-v",t),s(e,"translate3d(0, "+t*this.overviewSlideHeight+"px, 0)")}))})),Array.from(this.Reveal.getBackgroundsElement().childNodes).forEach(((e,i)=>{s(e,"translate3d("+i*this.overviewSlideWidth+"px, 0, 0)"),t(e,".slide-background").forEach(((e,t)=>{s(e,"translate3d(0, "+t*this.overviewSlideHeight+"px, 0)")}))}))}update(){const e=Math.min(window.innerWidth,window.innerHeight),t=Math.max(e/5,150)/e,i=this.Reveal.getIndices();this.Reveal.transformSlides({overview:["scale("+t+")","translateX("+-i.h*this.overviewSlideWidth+"px)","translateY("+-i.v*this.overviewSlideHeight+"px)"].join(" ")})}deactivate(){if(this.Reveal.getConfig().overview){this.active=!1,this.Reveal.getRevealElement().classList.remove("overview"),this.Reveal.getRevealElement().classList.add("overview-deactivating"),setTimeout((()=>{this.Reveal.getRevealElement().classList.remove("overview-deactivating")}),1),this.Reveal.getRevealElement().appendChild(this.Reveal.getBackgroundsElement()),t(this.Reveal.getRevealElement(),".slides section").forEach((e=>{s(e,""),e.removeEventListener("click",this.onSlideClicked,!0)})),t(this.Reveal.getBackgroundsElement(),".slide-background").forEach((e=>{s(e,"")})),this.Reveal.transformSlides({overview:""});const e=this.Reveal.getIndices();this.Reveal.slide(e.h,e.v),this.Reveal.layout(),this.Reveal.cueAutoSlide(),this.Reveal.dispatchEvent({type:"overviewhidden",data:{indexh:e.h,indexv:e.v,currentSlide:this.Reveal.getCurrentSlide()}})}}toggle(e){"boolean"==typeof e?e?this.activate():this.deactivate():this.isActive()?this.deactivate():this.activate()}isActive(){return this.active}onSlideClicked(e){if(this.isActive()){e.preventDefault();let t=e.target;for(;t&&!t.nodeName.match(/section/gi);)t=t.parentNode;if(t&&!t.classList.contains("disabled")&&(this.deactivate(),t.nodeName.match(/section/gi))){let e=parseInt(t.getAttribute("data-index-h"),10),i=parseInt(t.getAttribute("data-index-v"),10);this.Reveal.slide(e,i)}}}}class x{constructor(e){this.Reveal=e,this.shortcuts={},this.bindings={},this.onDocumentKeyDown=this.onDocumentKeyDown.bind(this),this.onDocumentKeyPress=this.onDocumentKeyPress.bind(this)}configure(e,t){"linear"===e.navigationMode?(this.shortcuts["→ , ↓ , SPACE , N , L , J"]="Next slide",this.shortcuts["← , ↑ , P , H , K"]="Previous slide"):(this.shortcuts["N , SPACE"]="Next slide",this.shortcuts["P , Shift SPACE"]="Previous slide",this.shortcuts["← , H"]="Navigate left",this.shortcuts["→ , L"]="Navigate right",this.shortcuts["↑ , K"]="Navigate up",this.shortcuts["↓ , J"]="Navigate down"),this.shortcuts["Alt + ←/↑/→/↓"]="Navigate without fragments",this.shortcuts["Shift + ←/↑/→/↓"]="Jump to first/last slide",this.shortcuts["B , ."]="Pause",this.shortcuts.F="Fullscreen",this.shortcuts["ESC, O"]="Slide overview"}bind(){document.addEventListener("keydown",this.onDocumentKeyDown,!1),document.addEventListener("keypress",this.onDocumentKeyPress,!1)}unbind(){document.removeEventListener("keydown",this.onDocumentKeyDown,!1),document.removeEventListener("keypress",this.onDocumentKeyPress,!1)}addKeyBinding(e,t){"object"==typeof e&&e.keyCode?this.bindings[e.keyCode]={callback:t,key:e.key,description:e.description}:this.bindings[e]={callback:t,key:null,description:null}}removeKeyBinding(e){delete this.bindings[e]}triggerKey(e){this.onDocumentKeyDown({keyCode:e})}registerKeyboardShortcut(e,t){this.shortcuts[e]=t}getShortcuts(){return this.shortcuts}getBindings(){return this.bindings}onDocumentKeyPress(e){e.shiftKey&&63===e.charCode&&this.Reveal.toggleHelp()}onDocumentKeyDown(e){let t=this.Reveal.getConfig();if("function"==typeof t.keyboardCondition&&!1===t.keyboardCondition(e))return!0;if("focused"===t.keyboardCondition&&!this.Reveal.isFocused())return!0;let i=e.keyCode,a=!this.Reveal.isAutoSliding();this.Reveal.onUserInput(e);let s=document.activeElement&&!0===document.activeElement.isContentEditable,n=document.activeElement&&document.activeElement.tagName&&/input|textarea/i.test(document.activeElement.tagName),r=document.activeElement&&document.activeElement.className&&/speaker-notes/i.test(document.activeElement.className),o=!(-1!==[32,37,38,39,40,78,80].indexOf(e.keyCode)&&e.shiftKey||e.altKey)&&(e.shiftKey||e.altKey||e.ctrlKey||e.metaKey);if(s||n||r||o)return;let l,d=[66,86,190,191];if("object"==typeof t.keyboard)for(l in t.keyboard)"togglePause"===t.keyboard[l]&&d.push(parseInt(l,10));if(this.Reveal.isPaused()&&-1===d.indexOf(i))return!1;let c="linear"===t.navigationMode||!this.Reveal.hasHorizontalSlides()||!this.Reveal.hasVerticalSlides(),h=!1;if("object"==typeof t.keyboard)for(l in t.keyboard)if(parseInt(l,10)===i){let i=t.keyboard[l];"function"==typeof i?i.apply(null,[e]):"string"==typeof i&&"function"==typeof this.Reveal[i]&&this.Reveal[i].call(),h=!0}if(!1===h)for(l in this.bindings)if(parseInt(l,10)===i){let t=this.bindings[l].callback;"function"==typeof t?t.apply(null,[e]):"string"==typeof t&&"function"==typeof this.Reveal[t]&&this.Reveal[t].call(),h=!0}!1===h&&(h=!0,80===i||33===i?this.Reveal.prev({skipFragments:e.altKey}):78===i||34===i?this.Reveal.next({skipFragments:e.altKey}):72===i||37===i?e.shiftKey?this.Reveal.slide(0):!this.Reveal.overview.isActive()&&c?this.Reveal.prev({skipFragments:e.altKey}):this.Reveal.left({skipFragments:e.altKey}):76===i||39===i?e.shiftKey?this.Reveal.slide(this.Reveal.getHorizontalSlides().length-1):!this.Reveal.overview.isActive()&&c?this.Reveal.next({skipFragments:e.altKey}):this.Reveal.right({skipFragments:e.altKey}):75===i||38===i?e.shiftKey?this.Reveal.slide(void 0,0):!this.Reveal.overview.isActive()&&c?this.Reveal.prev({skipFragments:e.altKey}):this.Reveal.up({skipFragments:e.altKey}):74===i||40===i?e.shiftKey?this.Reveal.slide(void 0,Number.MAX_VALUE):!this.Reveal.overview.isActive()&&c?this.Reveal.next({skipFragments:e.altKey}):this.Reveal.down({skipFragments:e.altKey}):36===i?this.Reveal.slide(0):35===i?this.Reveal.slide(this.Reveal.getHorizontalSlides().length-1):32===i?(this.Reveal.overview.isActive()&&this.Reveal.overview.deactivate(),e.shiftKey?this.Reveal.prev({skipFragments:e.altKey}):this.Reveal.next({skipFragments:e.altKey})):58===i||59===i||66===i||86===i||190===i||191===i?this.Reveal.togglePause():70===i?(e=>{let t=(e=e||document.documentElement).requestFullscreen||e.webkitRequestFullscreen||e.webkitRequestFullScreen||e.mozRequestFullScreen||e.msRequestFullscreen;t&&t.apply(e)})(t.embedded?this.Reveal.getViewportElement():document.documentElement):65===i?t.autoSlideStoppable&&this.Reveal.toggleAutoSlide(a):h=!1),h?e.preventDefault&&e.preventDefault():27!==i&&79!==i||(!1===this.Reveal.closeOverlay()&&this.Reveal.overview.toggle(),e.preventDefault&&e.preventDefault()),this.Reveal.cueAutoSlide()}}class P{constructor(e){var t,i,a;a=1e3,(i="MAX_REPLACE_STATE_FREQUENCY")in(t=this)?Object.defineProperty(t,i,{value:a,enumerable:!0,configurable:!0,writable:!0}):t[i]=a,this.Reveal=e,this.writeURLTimeout=0,this.replaceStateTimestamp=0,this.onWindowHashChange=this.onWindowHashChange.bind(this)}bind(){window.addEventListener("hashchange",this.onWindowHashChange,!1)}unbind(){window.removeEventListener("hashchange",this.onWindowHashChange,!1)}getIndicesFromHash(e=window.location.hash){let t=e.replace(/^#\/?/,""),i=t.split("/");if(/^[0-9]*$/.test(i[0])||!t.length){const e=this.Reveal.getConfig();let t,a=e.hashOneBasedIndex?1:0,s=parseInt(i[0],10)-a||0,n=parseInt(i[1],10)-a||0;return e.fragmentInURL&&(t=parseInt(i[2],10),isNaN(t)&&(t=void 0)),{h:s,v:n,f:t}}{let e,i;/\/[-\d]+$/g.test(t)&&(i=parseInt(t.split("/").pop(),10),i=isNaN(i)?void 0:i,t=t.split("/").shift());try{e=document.getElementById(decodeURIComponent(t))}catch(e){}if(e)return{...this.Reveal.getIndices(e),f:i}}return null}readURL(){const e=this.Reveal.getIndices(),t=this.getIndicesFromHash();t?t.h===e.h&&t.v===e.v&&void 0===t.f||this.Reveal.slide(t.h,t.v,t.f):this.Reveal.slide(e.h||0,e.v||0)}writeURL(e){let t=this.Reveal.getConfig(),i=this.Reveal.getCurrentSlide();if(clearTimeout(this.writeURLTimeout),"number"==typeof e)this.writeURLTimeout=setTimeout(this.writeURL,e);else if(i){let e=this.getHash();t.history?window.location.hash=e:t.hash&&("/"===e?this.debouncedReplaceState(window.location.pathname+window.location.search):this.debouncedReplaceState("#"+e))}}replaceState(e){window.history.replaceState(null,null,e),this.replaceStateTimestamp=Date.now()}debouncedReplaceState(e){clearTimeout(this.replaceStateTimeout),Date.now()-this.replaceStateTimestamp>this.MAX_REPLACE_STATE_FREQUENCY?this.replaceState(e):this.replaceStateTimeout=setTimeout((()=>this.replaceState(e)),this.MAX_REPLACE_STATE_FREQUENCY)}getHash(e){let t="/",i=e||this.Reveal.getCurrentSlide(),a=i?i.getAttribute("id"):null;a&&(a=encodeURIComponent(a));let s=this.Reveal.getIndices(e);if(this.Reveal.getConfig().fragmentInURL||(s.f=void 0),"string"==typeof a&&a.length)t="/"+a,s.f>=0&&(t+="/"+s.f);else{let e=this.Reveal.getConfig().hashOneBasedIndex?1:0;(s.h>0||s.v>0||s.f>=0)&&(t+=s.h+e),(s.v>0||s.f>=0)&&(t+="/"+(s.v+e)),s.f>=0&&(t+="/"+s.f)}return t}onWindowHashChange(e){this.readURL()}}class N{constructor(e){this.Reveal=e,this.onNavigateLeftClicked=this.onNavigateLeftClicked.bind(this),this.onNavigateRightClicked=this.onNavigateRightClicked.bind(this),this.onNavigateUpClicked=this.onNavigateUpClicked.bind(this),this.onNavigateDownClicked=this.onNavigateDownClicked.bind(this),this.onNavigatePrevClicked=this.onNavigatePrevClicked.bind(this),this.onNavigateNextClicked=this.onNavigateNextClicked.bind(this)}render(){const e=this.Reveal.getConfig().rtl,i=this.Reveal.getRevealElement();this.element=document.createElement("aside"),this.element.className="controls",this.element.innerHTML=`\n\t\t\t\n\t\t\t\n\t\t\t`,this.Reveal.getRevealElement().appendChild(this.element),this.controlsLeft=t(i,".navigate-left"),this.controlsRight=t(i,".navigate-right"),this.controlsUp=t(i,".navigate-up"),this.controlsDown=t(i,".navigate-down"),this.controlsPrev=t(i,".navigate-prev"),this.controlsNext=t(i,".navigate-next"),this.controlsRightArrow=this.element.querySelector(".navigate-right"),this.controlsLeftArrow=this.element.querySelector(".navigate-left"),this.controlsDownArrow=this.element.querySelector(".navigate-down")}configure(e,t){this.element.style.display=e.controls?"block":"none",this.element.setAttribute("data-controls-layout",e.controlsLayout),this.element.setAttribute("data-controls-back-arrows",e.controlsBackArrows)}bind(){let e=["touchstart","click"];v&&(e=["touchstart"]),e.forEach((e=>{this.controlsLeft.forEach((t=>t.addEventListener(e,this.onNavigateLeftClicked,!1))),this.controlsRight.forEach((t=>t.addEventListener(e,this.onNavigateRightClicked,!1))),this.controlsUp.forEach((t=>t.addEventListener(e,this.onNavigateUpClicked,!1))),this.controlsDown.forEach((t=>t.addEventListener(e,this.onNavigateDownClicked,!1))),this.controlsPrev.forEach((t=>t.addEventListener(e,this.onNavigatePrevClicked,!1))),this.controlsNext.forEach((t=>t.addEventListener(e,this.onNavigateNextClicked,!1)))}))}unbind(){["touchstart","click"].forEach((e=>{this.controlsLeft.forEach((t=>t.removeEventListener(e,this.onNavigateLeftClicked,!1))),this.controlsRight.forEach((t=>t.removeEventListener(e,this.onNavigateRightClicked,!1))),this.controlsUp.forEach((t=>t.removeEventListener(e,this.onNavigateUpClicked,!1))),this.controlsDown.forEach((t=>t.removeEventListener(e,this.onNavigateDownClicked,!1))),this.controlsPrev.forEach((t=>t.removeEventListener(e,this.onNavigatePrevClicked,!1))),this.controlsNext.forEach((t=>t.removeEventListener(e,this.onNavigateNextClicked,!1)))}))}update(){let e=this.Reveal.availableRoutes();[...this.controlsLeft,...this.controlsRight,...this.controlsUp,...this.controlsDown,...this.controlsPrev,...this.controlsNext].forEach((e=>{e.classList.remove("enabled","fragmented"),e.setAttribute("disabled","disabled")})),e.left&&this.controlsLeft.forEach((e=>{e.classList.add("enabled"),e.removeAttribute("disabled")})),e.right&&this.controlsRight.forEach((e=>{e.classList.add("enabled"),e.removeAttribute("disabled")})),e.up&&this.controlsUp.forEach((e=>{e.classList.add("enabled"),e.removeAttribute("disabled")})),e.down&&this.controlsDown.forEach((e=>{e.classList.add("enabled"),e.removeAttribute("disabled")})),(e.left||e.up)&&this.controlsPrev.forEach((e=>{e.classList.add("enabled"),e.removeAttribute("disabled")})),(e.right||e.down)&&this.controlsNext.forEach((e=>{e.classList.add("enabled"),e.removeAttribute("disabled")}));let t=this.Reveal.getCurrentSlide();if(t){let e=this.Reveal.fragments.availableRoutes();e.prev&&this.controlsPrev.forEach((e=>{e.classList.add("fragmented","enabled"),e.removeAttribute("disabled")})),e.next&&this.controlsNext.forEach((e=>{e.classList.add("fragmented","enabled"),e.removeAttribute("disabled")})),this.Reveal.isVerticalSlide(t)?(e.prev&&this.controlsUp.forEach((e=>{e.classList.add("fragmented","enabled"),e.removeAttribute("disabled")})),e.next&&this.controlsDown.forEach((e=>{e.classList.add("fragmented","enabled"),e.removeAttribute("disabled")}))):(e.prev&&this.controlsLeft.forEach((e=>{e.classList.add("fragmented","enabled"),e.removeAttribute("disabled")})),e.next&&this.controlsRight.forEach((e=>{e.classList.add("fragmented","enabled"),e.removeAttribute("disabled")})))}if(this.Reveal.getConfig().controlsTutorial){let t=this.Reveal.getIndices();!this.Reveal.hasNavigatedVertically()&&e.down?this.controlsDownArrow.classList.add("highlight"):(this.controlsDownArrow.classList.remove("highlight"),this.Reveal.getConfig().rtl?!this.Reveal.hasNavigatedHorizontally()&&e.left&&0===t.v?this.controlsLeftArrow.classList.add("highlight"):this.controlsLeftArrow.classList.remove("highlight"):!this.Reveal.hasNavigatedHorizontally()&&e.right&&0===t.v?this.controlsRightArrow.classList.add("highlight"):this.controlsRightArrow.classList.remove("highlight"))}}destroy(){this.unbind(),this.element.remove()}onNavigateLeftClicked(e){e.preventDefault(),this.Reveal.onUserInput(),"linear"===this.Reveal.getConfig().navigationMode?this.Reveal.prev():this.Reveal.left()}onNavigateRightClicked(e){e.preventDefault(),this.Reveal.onUserInput(),"linear"===this.Reveal.getConfig().navigationMode?this.Reveal.next():this.Reveal.right()}onNavigateUpClicked(e){e.preventDefault(),this.Reveal.onUserInput(),this.Reveal.up()}onNavigateDownClicked(e){e.preventDefault(),this.Reveal.onUserInput(),this.Reveal.down()}onNavigatePrevClicked(e){e.preventDefault(),this.Reveal.onUserInput(),this.Reveal.prev()}onNavigateNextClicked(e){e.preventDefault(),this.Reveal.onUserInput(),this.Reveal.next()}}class M{constructor(e){this.Reveal=e,this.onProgressClicked=this.onProgressClicked.bind(this)}render(){this.element=document.createElement("div"),this.element.className="progress",this.Reveal.getRevealElement().appendChild(this.element),this.bar=document.createElement("span"),this.element.appendChild(this.bar)}configure(e,t){this.element.style.display=e.progress?"block":"none"}bind(){this.Reveal.getConfig().progress&&this.element&&this.element.addEventListener("click",this.onProgressClicked,!1)}unbind(){this.Reveal.getConfig().progress&&this.element&&this.element.removeEventListener("click",this.onProgressClicked,!1)}update(){if(this.Reveal.getConfig().progress&&this.bar){let e=this.Reveal.getProgress();this.Reveal.getTotalSlides()<2&&(e=0),this.bar.style.transform="scaleX("+e+")"}}getMaxWidth(){return this.Reveal.getRevealElement().offsetWidth}onProgressClicked(e){this.Reveal.onUserInput(e),e.preventDefault();let t=this.Reveal.getSlides(),i=t.length,a=Math.floor(e.clientX/this.getMaxWidth()*i);this.Reveal.getConfig().rtl&&(a=i-a);let s=this.Reveal.getIndices(t[a]);this.Reveal.slide(s.h,s.v)}destroy(){this.element.remove()}}class D{constructor(e){this.Reveal=e,this.lastMouseWheelStep=0,this.cursorHidden=!1,this.cursorInactiveTimeout=0,this.onDocumentCursorActive=this.onDocumentCursorActive.bind(this),this.onDocumentMouseScroll=this.onDocumentMouseScroll.bind(this)}configure(e,t){e.mouseWheel?(document.addEventListener("DOMMouseScroll",this.onDocumentMouseScroll,!1),document.addEventListener("mousewheel",this.onDocumentMouseScroll,!1)):(document.removeEventListener("DOMMouseScroll",this.onDocumentMouseScroll,!1),document.removeEventListener("mousewheel",this.onDocumentMouseScroll,!1)),e.hideInactiveCursor?(document.addEventListener("mousemove",this.onDocumentCursorActive,!1),document.addEventListener("mousedown",this.onDocumentCursorActive,!1)):(this.showCursor(),document.removeEventListener("mousemove",this.onDocumentCursorActive,!1),document.removeEventListener("mousedown",this.onDocumentCursorActive,!1))}showCursor(){this.cursorHidden&&(this.cursorHidden=!1,this.Reveal.getRevealElement().style.cursor="")}hideCursor(){!1===this.cursorHidden&&(this.cursorHidden=!0,this.Reveal.getRevealElement().style.cursor="none")}destroy(){this.showCursor(),document.removeEventListener("DOMMouseScroll",this.onDocumentMouseScroll,!1),document.removeEventListener("mousewheel",this.onDocumentMouseScroll,!1),document.removeEventListener("mousemove",this.onDocumentCursorActive,!1),document.removeEventListener("mousedown",this.onDocumentCursorActive,!1)}onDocumentCursorActive(e){this.showCursor(),clearTimeout(this.cursorInactiveTimeout),this.cursorInactiveTimeout=setTimeout(this.hideCursor.bind(this),this.Reveal.getConfig().hideCursorTime)}onDocumentMouseScroll(e){if(Date.now()-this.lastMouseWheelStep>1e3){this.lastMouseWheelStep=Date.now();let t=e.detail||-e.wheelDelta;t>0?this.Reveal.next():t<0&&this.Reveal.prev()}}}const I=(e,t)=>{const i=document.createElement("script");i.type="text/javascript",i.async=!1,i.defer=!1,i.src=e,"function"==typeof t&&(i.onload=i.onreadystatechange=e=>{("load"===e.type||/loaded|complete/.test(i.readyState))&&(i.onload=i.onreadystatechange=i.onerror=null,t())},i.onerror=e=>{i.onload=i.onreadystatechange=i.onerror=null,t(new Error("Failed loading script: "+i.src+"\n"+e))});const a=document.querySelector("head");a.insertBefore(i,a.lastChild)};class T{constructor(e){this.Reveal=e,this.state="idle",this.registeredPlugins={},this.asyncDependencies=[]}load(e,t){return this.state="loading",e.forEach(this.registerPlugin.bind(this)),new Promise((e=>{let i=[],a=0;if(t.forEach((e=>{e.condition&&!e.condition()||(e.async?this.asyncDependencies.push(e):i.push(e))})),i.length){a=i.length;const t=t=>{t&&"function"==typeof t.callback&&t.callback(),0==--a&&this.initPlugins().then(e)};i.forEach((e=>{"string"==typeof e.id?(this.registerPlugin(e),t(e)):"string"==typeof e.src?I(e.src,(()=>t(e))):(console.warn("Unrecognized plugin format",e),t())}))}else this.initPlugins().then(e)}))}initPlugins(){return new Promise((e=>{let t=Object.values(this.registeredPlugins),i=t.length;if(0===i)this.loadAsync().then(e);else{let a,s=()=>{0==--i?this.loadAsync().then(e):a()},n=0;a=()=>{let e=t[n++];if("function"==typeof e.init){let t=e.init(this.Reveal);t&&"function"==typeof t.then?t.then(s):s()}else s()},a()}}))}loadAsync(){return this.state="loaded",this.asyncDependencies.length&&this.asyncDependencies.forEach((e=>{I(e.src,e.callback)})),Promise.resolve()}registerPlugin(e){2===arguments.length&&"string"==typeof arguments[0]?(e=arguments[1]).id=arguments[0]:"function"==typeof e&&(e=e());let t=e.id;"string"!=typeof t?console.warn("Unrecognized plugin format; can't find plugin.id",e):void 0===this.registeredPlugins[t]?(this.registeredPlugins[t]=e,"loaded"===this.state&&"function"==typeof e.init&&e.init(this.Reveal)):console.warn('reveal.js: "'+t+'" plugin has already been registered')}hasPlugin(e){return!!this.registeredPlugins[e]}getPlugin(e){return this.registeredPlugins[e]}getRegisteredPlugins(){return this.registeredPlugins}destroy(){Object.values(this.registeredPlugins).forEach((e=>{"function"==typeof e.destroy&&e.destroy()})),this.registeredPlugins={},this.asyncDependencies=[]}}class F{constructor(e){this.Reveal=e}async setupPDF(){const e=this.Reveal.getConfig(),i=t(this.Reveal.getRevealElement(),".slides section"),a=e.slideNumber&&/all|print/i.test(e.showSlideNumber),s=this.Reveal.getComputedSlideSize(window.innerWidth,window.innerHeight),n=Math.floor(s.width*(1+e.margin)),r=Math.floor(s.height*(1+e.margin)),o=s.width,d=s.height;await new Promise(requestAnimationFrame),l("@page{size:"+n+"px "+r+"px; margin: 0px;}"),l(".reveal section>img, .reveal section>video, .reveal section>iframe{max-width: "+o+"px; max-height:"+d+"px}"),document.documentElement.classList.add("print-pdf"),document.body.style.width=n+"px",document.body.style.height=r+"px";const c=document.querySelector(".reveal-viewport");let h;if(c){const e=window.getComputedStyle(c);e&&e.background&&(h=e.background)}await new Promise(requestAnimationFrame),this.Reveal.layoutSlideContents(o,d),await new Promise(requestAnimationFrame);const u=i.map((e=>e.scrollHeight)),g=[],v=i[0].parentNode;let p=1;i.forEach((function(i,s){if(!1===i.classList.contains("stack")){let l=(n-o)/2,c=(r-d)/2;const v=u[s];let m=Math.max(Math.ceil(v/r),1);m=Math.min(m,e.pdfMaxPagesPerSlide),(1===m&&e.center||i.classList.contains("center"))&&(c=Math.max((r-v)/2,0));const f=document.createElement("div");if(g.push(f),f.className="pdf-page",f.style.height=(r+e.pdfPageHeightOffset)*m+"px",h&&(f.style.background=h),f.appendChild(i),i.style.left=l+"px",i.style.top=c+"px",i.style.width=o+"px",this.Reveal.slideContent.layout(i),i.slideBackgroundElement&&f.insertBefore(i.slideBackgroundElement,i),e.showNotes){const t=this.Reveal.getSlideNotes(i);if(t){const i=8,a="string"==typeof e.showNotes?e.showNotes:"inline",s=document.createElement("div");s.classList.add("speaker-notes"),s.classList.add("speaker-notes-pdf"),s.setAttribute("data-layout",a),s.innerHTML=t,"separate-page"===a?g.push(s):(s.style.left=i+"px",s.style.bottom=i+"px",s.style.width=n-2*i+"px",f.appendChild(s))}}if(a){const e=document.createElement("div");e.classList.add("slide-number"),e.classList.add("slide-number-pdf"),e.innerHTML=p++,f.appendChild(e)}if(e.pdfSeparateFragments){const e=this.Reveal.fragments.sort(f.querySelectorAll(".fragment"),!0);let t;e.forEach((function(e,i){t&&t.forEach((function(e){e.classList.remove("current-fragment")})),e.forEach((function(e){e.classList.add("visible","current-fragment")}),this);const s=f.cloneNode(!0);if(a){const e=i+1;s.querySelector(".slide-number-pdf").innerHTML+="."+e}g.push(s),t=e}),this),e.forEach((function(e){e.forEach((function(e){e.classList.remove("visible","current-fragment")}))}))}else t(f,".fragment:not(.fade-out)").forEach((function(e){e.classList.add("visible")}))}}),this),await new Promise(requestAnimationFrame),g.forEach((e=>v.appendChild(e))),this.Reveal.slideContent.layout(this.Reveal.getSlidesElement()),this.Reveal.dispatchEvent({type:"pdf-ready"})}isPrintingPDF(){return/print-pdf/gi.test(window.location.search)}}class z{constructor(e){this.Reveal=e,this.touchStartX=0,this.touchStartY=0,this.touchStartCount=0,this.touchCaptured=!1,this.onPointerDown=this.onPointerDown.bind(this),this.onPointerMove=this.onPointerMove.bind(this),this.onPointerUp=this.onPointerUp.bind(this),this.onTouchStart=this.onTouchStart.bind(this),this.onTouchMove=this.onTouchMove.bind(this),this.onTouchEnd=this.onTouchEnd.bind(this)}bind(){let e=this.Reveal.getRevealElement();"onpointerdown"in window?(e.addEventListener("pointerdown",this.onPointerDown,!1),e.addEventListener("pointermove",this.onPointerMove,!1),e.addEventListener("pointerup",this.onPointerUp,!1)):window.navigator.msPointerEnabled?(e.addEventListener("MSPointerDown",this.onPointerDown,!1),e.addEventListener("MSPointerMove",this.onPointerMove,!1),e.addEventListener("MSPointerUp",this.onPointerUp,!1)):(e.addEventListener("touchstart",this.onTouchStart,!1),e.addEventListener("touchmove",this.onTouchMove,!1),e.addEventListener("touchend",this.onTouchEnd,!1))}unbind(){let e=this.Reveal.getRevealElement();e.removeEventListener("pointerdown",this.onPointerDown,!1),e.removeEventListener("pointermove",this.onPointerMove,!1),e.removeEventListener("pointerup",this.onPointerUp,!1),e.removeEventListener("MSPointerDown",this.onPointerDown,!1),e.removeEventListener("MSPointerMove",this.onPointerMove,!1),e.removeEventListener("MSPointerUp",this.onPointerUp,!1),e.removeEventListener("touchstart",this.onTouchStart,!1),e.removeEventListener("touchmove",this.onTouchMove,!1),e.removeEventListener("touchend",this.onTouchEnd,!1)}isSwipePrevented(e){if(n(e,"video, audio"))return!0;for(;e&&"function"==typeof e.hasAttribute;){if(e.hasAttribute("data-prevent-swipe"))return!0;e=e.parentNode}return!1}onTouchStart(e){if(this.isSwipePrevented(e.target))return!0;this.touchStartX=e.touches[0].clientX,this.touchStartY=e.touches[0].clientY,this.touchStartCount=e.touches.length}onTouchMove(e){if(this.isSwipePrevented(e.target))return!0;let t=this.Reveal.getConfig();if(this.touchCaptured)v&&e.preventDefault();else{this.Reveal.onUserInput(e);let i=e.touches[0].clientX,a=e.touches[0].clientY;if(1===e.touches.length&&2!==this.touchStartCount){let s=this.Reveal.availableRoutes({includeFragments:!0}),n=i-this.touchStartX,r=a-this.touchStartY;n>40&&Math.abs(n)>Math.abs(r)?(this.touchCaptured=!0,"linear"===t.navigationMode?t.rtl?this.Reveal.next():this.Reveal.prev():this.Reveal.left()):n<-40&&Math.abs(n)>Math.abs(r)?(this.touchCaptured=!0,"linear"===t.navigationMode?t.rtl?this.Reveal.prev():this.Reveal.next():this.Reveal.right()):r>40&&s.up?(this.touchCaptured=!0,"linear"===t.navigationMode?this.Reveal.prev():this.Reveal.up()):r<-40&&s.down&&(this.touchCaptured=!0,"linear"===t.navigationMode?this.Reveal.next():this.Reveal.down()),t.embedded?(this.touchCaptured||this.Reveal.isVerticalSlide())&&e.preventDefault():e.preventDefault()}}}onTouchEnd(e){this.touchCaptured=!1}onPointerDown(e){e.pointerType!==e.MSPOINTER_TYPE_TOUCH&&"touch"!==e.pointerType||(e.touches=[{clientX:e.clientX,clientY:e.clientY}],this.onTouchStart(e))}onPointerMove(e){e.pointerType!==e.MSPOINTER_TYPE_TOUCH&&"touch"!==e.pointerType||(e.touches=[{clientX:e.clientX,clientY:e.clientY}],this.onTouchMove(e))}onPointerUp(e){e.pointerType!==e.MSPOINTER_TYPE_TOUCH&&"touch"!==e.pointerType||(e.touches=[{clientX:e.clientX,clientY:e.clientY}],this.onTouchEnd(e))}}class H{constructor(e){this.Reveal=e,this.onRevealPointerDown=this.onRevealPointerDown.bind(this),this.onDocumentPointerDown=this.onDocumentPointerDown.bind(this)}configure(e,t){e.embedded?this.blur():(this.focus(),this.unbind())}bind(){this.Reveal.getConfig().embedded&&this.Reveal.getRevealElement().addEventListener("pointerdown",this.onRevealPointerDown,!1)}unbind(){this.Reveal.getRevealElement().removeEventListener("pointerdown",this.onRevealPointerDown,!1),document.removeEventListener("pointerdown",this.onDocumentPointerDown,!1)}focus(){"focus"!==this.state&&(this.Reveal.getRevealElement().classList.add("focused"),document.addEventListener("pointerdown",this.onDocumentPointerDown,!1)),this.state="focus"}blur(){"blur"!==this.state&&(this.Reveal.getRevealElement().classList.remove("focused"),document.removeEventListener("pointerdown",this.onDocumentPointerDown,!1)),this.state="blur"}isFocused(){return"focus"===this.state}destroy(){this.Reveal.getRevealElement().classList.remove("focused")}onRevealPointerDown(e){this.focus()}onDocumentPointerDown(e){let t=r(e.target,".reveal");t&&t===this.Reveal.getRevealElement()||this.blur()}}class q{constructor(e){this.Reveal=e}render(){this.element=document.createElement("div"),this.element.className="speaker-notes",this.element.setAttribute("data-prevent-swipe",""),this.element.setAttribute("tabindex","0"),this.Reveal.getRevealElement().appendChild(this.element)}configure(e,t){e.showNotes&&this.element.setAttribute("data-layout","string"==typeof e.showNotes?e.showNotes:"inline")}update(){this.Reveal.getConfig().showNotes&&this.element&&this.Reveal.getCurrentSlide()&&!this.Reveal.print.isPrintingPDF()&&(this.element.innerHTML=this.getSlideNotes()||'No notes on this slide.')}updateVisibility(){this.Reveal.getConfig().showNotes&&this.hasNotes()&&!this.Reveal.print.isPrintingPDF()?this.Reveal.getRevealElement().classList.add("show-notes"):this.Reveal.getRevealElement().classList.remove("show-notes")}hasNotes(){return this.Reveal.getSlidesElement().querySelectorAll("[data-notes], aside.notes").length>0}isSpeakerNotesWindow(){return!!window.location.search.match(/receiver/gi)}getSlideNotes(e=this.Reveal.getCurrentSlide()){if(e.hasAttribute("data-notes"))return e.getAttribute("data-notes");let t=e.querySelector("aside.notes");return t?t.innerHTML:null}destroy(){this.element.remove()}}class B{constructor(e,t){this.diameter=100,this.diameter2=this.diameter/2,this.thickness=6,this.playing=!1,this.progress=0,this.progressOffset=1,this.container=e,this.progressCheck=t,this.canvas=document.createElement("canvas"),this.canvas.className="playback",this.canvas.width=this.diameter,this.canvas.height=this.diameter,this.canvas.style.width=this.diameter2+"px",this.canvas.style.height=this.diameter2+"px",this.context=this.canvas.getContext("2d"),this.container.appendChild(this.canvas),this.render()}setPlaying(e){const t=this.playing;this.playing=e,!t&&this.playing?this.animate():this.render()}animate(){const e=this.progress;this.progress=this.progressCheck(),e>.8&&this.progress<.2&&(this.progressOffset=this.progress),this.render(),this.playing&&requestAnimationFrame(this.animate.bind(this))}render(){let e=this.playing?this.progress:0,t=this.diameter2-this.thickness,i=this.diameter2,a=this.diameter2,s=28;this.progressOffset+=.1*(1-this.progressOffset);const n=-Math.PI/2+e*(2*Math.PI),r=-Math.PI/2+this.progressOffset*(2*Math.PI);this.context.save(),this.context.clearRect(0,0,this.diameter,this.diameter),this.context.beginPath(),this.context.arc(i,a,t+4,0,2*Math.PI,!1),this.context.fillStyle="rgba( 0, 0, 0, 0.4 )",this.context.fill(),this.context.beginPath(),this.context.arc(i,a,t,0,2*Math.PI,!1),this.context.lineWidth=this.thickness,this.context.strokeStyle="rgba( 255, 255, 255, 0.2 )",this.context.stroke(),this.playing&&(this.context.beginPath(),this.context.arc(i,a,t,r,n,!1),this.context.lineWidth=this.thickness,this.context.strokeStyle="#fff",this.context.stroke()),this.context.translate(i-14,a-14),this.playing?(this.context.fillStyle="#fff",this.context.fillRect(0,0,10,s),this.context.fillRect(18,0,10,s)):(this.context.beginPath(),this.context.translate(4,0),this.context.moveTo(0,0),this.context.lineTo(24,14),this.context.lineTo(0,s),this.context.fillStyle="#fff",this.context.fill()),this.context.restore()}on(e,t){this.canvas.addEventListener(e,t,!1)}off(e,t){this.canvas.removeEventListener(e,t,!1)}destroy(){this.playing=!1,this.canvas.parentNode&&this.container.removeChild(this.canvas)}}var O={width:960,height:700,margin:.04,minScale:.2,maxScale:2,controls:!0,controlsTutorial:!0,controlsLayout:"bottom-right",controlsBackArrows:"faded",progress:!0,slideNumber:!1,showSlideNumber:"all",hashOneBasedIndex:!1,hash:!1,respondToHashChanges:!0,history:!1,keyboard:!0,keyboardCondition:null,disableLayout:!1,overview:!0,center:!0,touch:!0,loop:!1,rtl:!1,navigationMode:"default",shuffle:!1,fragments:!0,fragmentInURL:!0,embedded:!1,help:!0,pause:!0,showNotes:!1,showHiddenSlides:!1,autoPlayMedia:null,preloadIframes:null,autoAnimate:!0,autoAnimateMatcher:null,autoAnimateEasing:"ease",autoAnimateDuration:1,autoAnimateUnmatched:!0,autoAnimateStyles:["opacity","color","background-color","padding","font-size","line-height","letter-spacing","border-width","border-color","border-radius","outline","outline-offset"],autoSlide:0,autoSlideStoppable:!0,autoSlideMethod:null,defaultTiming:null,mouseWheel:!1,previewLinks:!1,postMessage:!0,postMessageEvents:!1,focusBodyOnPageVisibilityChange:!0,transition:"slide",transitionSpeed:"default",backgroundTransition:"fade",parallaxBackgroundImage:"",parallaxBackgroundSize:"",parallaxBackgroundRepeat:"",parallaxBackgroundPosition:"",parallaxBackgroundHorizontal:null,parallaxBackgroundVertical:null,pdfMaxPagesPerSlide:Number.POSITIVE_INFINITY,pdfSeparateFragments:!0,pdfPageHeightOffset:-1,viewDistance:3,mobileViewDistance:2,display:"block",hideInactiveCursor:!0,hideCursorTime:5e3,dependencies:[],plugins:[]};function U(n,l){arguments.length<2&&(l=arguments[0],n=document.querySelector(".reveal"));const h={};let u,v,p,m,f,w={},S=!1,A={hasNavigatedHorizontally:!1,hasNavigatedVertically:!1},I=[],U=1,W={layout:"",overview:""},K={},V="idle",$=0,j=0,X=-1,Y=!1,_=new b(h),G=new y(h),J=new k(h),Q=new E(h),Z=new L(h),ee=new C(h),te=new x(h),ie=new P(h),ae=new N(h),se=new M(h),ne=new D(h),re=new T(h),oe=new F(h),le=new H(h),de=new z(h),ce=new q(h);function he(e){if(!n)throw'Unable to find presentation root (
).';if(K.wrapper=n,K.slides=n.querySelector(".slides"),!K.slides)throw'Unable to find slides container (
).';return w={...O,...w,...l,...e,...d()},ue(),window.addEventListener("load",He,!1),re.load(w.plugins,w.dependencies).then(ge),new Promise((e=>h.on("ready",e)))}function ue(){!0===w.embedded?K.viewport=r(n,".reveal-viewport")||n:(K.viewport=document.body,document.documentElement.classList.add("reveal-full-page")),K.viewport.classList.add("reveal-viewport")}function ge(){S=!0,ve(),pe(),Ee(),ye(),we(),tt(),Re(),ie.readURL(),Q.update(!0),setTimeout((()=>{K.slides.classList.remove("no-transition"),K.wrapper.classList.add("ready"),Pe({type:"ready",data:{indexh:u,indexv:v,currentSlide:m}})}),1),oe.isPrintingPDF()&&(Ae(),"complete"===document.readyState?oe.setupPDF():window.addEventListener("load",(()=>{oe.setupPDF()})))}function ve(){w.showHiddenSlides||t(K.wrapper,'section[data-visibility="hidden"]').forEach((e=>{e.parentNode.removeChild(e)}))}function pe(){K.slides.classList.add("no-transition"),g?K.wrapper.classList.add("no-hover"):K.wrapper.classList.remove("no-hover"),Q.render(),G.render(),ae.render(),se.render(),ce.render(),K.pauseOverlay=o(K.wrapper,"div","pause-overlay",w.controls?'':null),K.statusElement=me(),K.wrapper.setAttribute("role","application")}function me(){let e=K.wrapper.querySelector(".aria-status");return e||(e=document.createElement("div"),e.style.position="absolute",e.style.height="1px",e.style.width="1px",e.style.overflow="hidden",e.style.clip="rect( 1px, 1px, 1px, 1px )",e.classList.add("aria-status"),e.setAttribute("aria-live","polite"),e.setAttribute("aria-atomic","true"),K.wrapper.appendChild(e)),e}function fe(e){K.statusElement.textContent=e}function be(e){let t="";if(3===e.nodeType)t+=e.textContent;else if(1===e.nodeType){let i=e.getAttribute("aria-hidden"),a="none"===window.getComputedStyle(e).display;"true"===i||a||Array.from(e.childNodes).forEach((e=>{t+=be(e)}))}return t=t.trim(),""===t?"":t+" "}function ye(){setInterval((()=>{0===K.wrapper.scrollTop&&0===K.wrapper.scrollLeft||(K.wrapper.scrollTop=0,K.wrapper.scrollLeft=0)}),1e3)}function we(){document.addEventListener("fullscreenchange",Bt),document.addEventListener("webkitfullscreenchange",Bt)}function Ee(){w.postMessage&&window.addEventListener("message",Tt,!1)}function Re(t){const a={...w};if("object"==typeof t&&e(w,t),!1===h.isReady())return;const s=K.wrapper.querySelectorAll(".slides section").length;K.wrapper.classList.remove(a.transition),K.wrapper.classList.add(w.transition),K.wrapper.setAttribute("data-transition-speed",w.transitionSpeed),K.wrapper.setAttribute("data-background-transition",w.backgroundTransition),K.viewport.style.setProperty("--slide-width",w.width+"px"),K.viewport.style.setProperty("--slide-height",w.height+"px"),w.shuffle&&it(),i(K.wrapper,"embedded",w.embedded),i(K.wrapper,"rtl",w.rtl),i(K.wrapper,"center",w.center),!1===w.pause&&Xe(),w.previewLinks?(Me(),De("[data-preview-link=false]")):(De(),Me("[data-preview-link]:not([data-preview-link=false])")),J.reset(),f&&(f.destroy(),f=null),s>1&&w.autoSlide&&w.autoSlideStoppable&&(f=new B(K.wrapper,(()=>Math.min(Math.max((Date.now()-X)/$,0),1))),f.on("click",Ut),Y=!1),"default"!==w.navigationMode?K.wrapper.setAttribute("data-navigation-mode",w.navigationMode):K.wrapper.removeAttribute("data-navigation-mode"),ce.configure(w,a),le.configure(w,a),ne.configure(w,a),ae.configure(w,a),se.configure(w,a),te.configure(w,a),Z.configure(w,a),G.configure(w,a),Ze()}function Se(){window.addEventListener("resize",Ht,!1),w.touch&&de.bind(),w.keyboard&&te.bind(),w.progress&&se.bind(),w.respondToHashChanges&&ie.bind(),ae.bind(),le.bind(),K.slides.addEventListener("click",zt,!1),K.slides.addEventListener("transitionend",Ft,!1),K.pauseOverlay.addEventListener("click",Xe,!1),w.focusBodyOnPageVisibilityChange&&document.addEventListener("visibilitychange",qt,!1)}function Ae(){de.unbind(),le.unbind(),te.unbind(),ae.unbind(),se.unbind(),ie.unbind(),window.removeEventListener("resize",Ht,!1),K.slides.removeEventListener("click",zt,!1),K.slides.removeEventListener("transitionend",Ft,!1),K.pauseOverlay.removeEventListener("click",Xe,!1)}function ke(){Ae(),At(),De(),ce.destroy(),le.destroy(),re.destroy(),ne.destroy(),ae.destroy(),se.destroy(),Q.destroy(),G.destroy(),document.removeEventListener("fullscreenchange",Bt),document.removeEventListener("webkitfullscreenchange",Bt),document.removeEventListener("visibilitychange",qt,!1),window.removeEventListener("message",Tt,!1),window.removeEventListener("load",He,!1),K.pauseOverlay&&K.pauseOverlay.remove(),K.statusElement&&K.statusElement.remove(),document.documentElement.classList.remove("reveal-full-page"),K.wrapper.classList.remove("ready","center","has-horizontal-slides","has-vertical-slides"),K.wrapper.removeAttribute("data-transition-speed"),K.wrapper.removeAttribute("data-background-transition"),K.viewport.classList.remove("reveal-viewport"),K.viewport.style.removeProperty("--slide-width"),K.viewport.style.removeProperty("--slide-height"),K.slides.style.removeProperty("width"),K.slides.style.removeProperty("height"),K.slides.style.removeProperty("zoom"),K.slides.style.removeProperty("left"),K.slides.style.removeProperty("top"),K.slides.style.removeProperty("bottom"),K.slides.style.removeProperty("right"),K.slides.style.removeProperty("transform"),Array.from(K.wrapper.querySelectorAll(".slides section")).forEach((e=>{e.style.removeProperty("display"),e.style.removeProperty("top"),e.removeAttribute("hidden"),e.removeAttribute("aria-hidden")}))}function Le(e,t,i){n.addEventListener(e,t,i)}function Ce(e,t,i){n.removeEventListener(e,t,i)}function xe(e){"string"==typeof e.layout&&(W.layout=e.layout),"string"==typeof e.overview&&(W.overview=e.overview),W.layout?s(K.slides,W.layout+" "+W.overview):s(K.slides,W.overview)}function Pe({target:t=K.wrapper,type:i,data:a,bubbles:s=!0}){let n=document.createEvent("HTMLEvents",1,2);return n.initEvent(i,s,!0),e(n,a),t.dispatchEvent(n),t===K.wrapper&&Ne(i),n}function Ne(t,i){if(w.postMessageEvents&&window.parent!==window.self){let a={namespace:"reveal",eventName:t,state:Et()};e(a,i),window.parent.postMessage(JSON.stringify(a),"*")}}function Me(e="a"){Array.from(K.wrapper.querySelectorAll(e)).forEach((e=>{/^(http|www)/gi.test(e.getAttribute("href"))&&e.addEventListener("click",Ot,!1)}))}function De(e="a"){Array.from(K.wrapper.querySelectorAll(e)).forEach((e=>{/^(http|www)/gi.test(e.getAttribute("href"))&&e.removeEventListener("click",Ot,!1)}))}function Ie(e){ze(),K.overlay=document.createElement("div"),K.overlay.classList.add("overlay"),K.overlay.classList.add("overlay-preview"),K.wrapper.appendChild(K.overlay),K.overlay.innerHTML=`
\n\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\tUnable to load iframe. This is likely due to the site's policy (x-frame-options).\n\t\t\t\t\n\t\t\t
`,K.overlay.querySelector("iframe").addEventListener("load",(e=>{K.overlay.classList.add("loaded")}),!1),K.overlay.querySelector(".close").addEventListener("click",(e=>{ze(),e.preventDefault()}),!1),K.overlay.querySelector(".external").addEventListener("click",(e=>{ze()}),!1)}function Te(e){"boolean"==typeof e?e?Fe():ze():K.overlay?ze():Fe()}function Fe(){if(w.help){ze(),K.overlay=document.createElement("div"),K.overlay.classList.add("overlay"),K.overlay.classList.add("overlay-help"),K.wrapper.appendChild(K.overlay);let e='

Keyboard Shortcuts


',t=te.getShortcuts(),i=te.getBindings();e+="";for(let i in t)e+=``;for(let t in i)i[t].key&&i[t].description&&(e+=``);e+="
KEYACTION
${i}${t[i]}
${i[t].key}${i[t].description}
",K.overlay.innerHTML=`\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
${e}
\n\t\t\t\t
\n\t\t\t`,K.overlay.querySelector(".close").addEventListener("click",(e=>{ze(),e.preventDefault()}),!1)}}function ze(){return!!K.overlay&&(K.overlay.parentNode.removeChild(K.overlay),K.overlay=null,!0)}function He(){if(K.wrapper&&!oe.isPrintingPDF()){if(!w.disableLayout){g&&!w.embedded&&document.documentElement.style.setProperty("--vh",.01*window.innerHeight+"px");const e=Be(),t=U;qe(w.width,w.height),K.slides.style.width=e.width+"px",K.slides.style.height=e.height+"px",U=Math.min(e.presentationWidth/e.width,e.presentationHeight/e.height),U=Math.max(U,w.minScale),U=Math.min(U,w.maxScale),1===U?(K.slides.style.zoom="",K.slides.style.left="",K.slides.style.top="",K.slides.style.bottom="",K.slides.style.right="",xe({layout:""})):(K.slides.style.zoom="",K.slides.style.left="50%",K.slides.style.top="50%",K.slides.style.bottom="auto",K.slides.style.right="auto",xe({layout:"translate(-50%, -50%) scale("+U+")"}));const i=Array.from(K.wrapper.querySelectorAll(".slides section"));for(let t=0,a=i.length;t .stretch, section > .r-stretch").forEach((t=>{let a=c(t,i);if(/(img|video)/gi.test(t.nodeName)){const i=t.naturalWidth||t.videoWidth,s=t.naturalHeight||t.videoHeight,n=Math.min(e/i,a/s);t.style.width=i*n+"px",t.style.height=s*n+"px"}else t.style.width=e+"px",t.style.height=a+"px"}))}function Be(e,t){const i={width:w.width,height:w.height,presentationWidth:e||K.wrapper.offsetWidth,presentationHeight:t||K.wrapper.offsetHeight};return i.presentationWidth-=i.presentationWidth*w.margin,i.presentationHeight-=i.presentationHeight*w.margin,"string"==typeof i.width&&/%$/.test(i.width)&&(i.width=parseInt(i.width,10)/100*i.presentationWidth),"string"==typeof i.height&&/%$/.test(i.height)&&(i.height=parseInt(i.height,10)/100*i.presentationHeight),i}function Oe(e,t){"object"==typeof e&&"function"==typeof e.setAttribute&&e.setAttribute("data-previous-indexv",t||0)}function Ue(e){if("object"==typeof e&&"function"==typeof e.setAttribute&&e.classList.contains("stack")){const t=e.hasAttribute("data-start-indexv")?"data-start-indexv":"data-previous-indexv";return parseInt(e.getAttribute(t)||0,10)}return 0}function We(e=m){return e&&e.parentNode&&!!e.parentNode.nodeName.match(/section/i)}function Ke(){return!(!m||!We(m))&&!m.nextElementSibling}function Ve(){return 0===u&&0===v}function $e(){return!!m&&(!m.nextElementSibling&&(!We(m)||!m.parentNode.nextElementSibling))}function je(){if(w.pause){const e=K.wrapper.classList.contains("paused");At(),K.wrapper.classList.add("paused"),!1===e&&Pe({type:"paused"})}}function Xe(){const e=K.wrapper.classList.contains("paused");K.wrapper.classList.remove("paused"),St(),e&&Pe({type:"resumed"})}function Ye(e){"boolean"==typeof e?e?je():Xe():_e()?Xe():je()}function _e(){return K.wrapper.classList.contains("paused")}function Ge(e){"boolean"==typeof e?e?Lt():kt():Y?Lt():kt()}function Je(){return!(!$||Y)}function Qe(e,t,i,a){if(Pe({type:"beforeslidechange",data:{indexh:void 0===e?u:e,indexv:void 0===t?v:t,origin:a}}).defaultPrevented)return;p=m;const s=K.wrapper.querySelectorAll(".slides>section");if(0===s.length)return;void 0!==t||ee.isActive()||(t=Ue(s[e])),p&&p.parentNode&&p.parentNode.classList.contains("stack")&&Oe(p.parentNode,v);const n=I.concat();I.length=0;let r=u||0,o=v||0;u=at(".slides>section",void 0===e?u:e),v=at(".slides>section.present>section",void 0===t?v:t);let l=u!==r||v!==o;l||(p=null);let d=s[u],c=d.querySelectorAll("section");m=c[v]||d;let h=!1;l&&p&&m&&!ee.isActive()&&(p.hasAttribute("data-auto-animate")&&m.hasAttribute("data-auto-animate")&&p.getAttribute("data-auto-animate-id")===m.getAttribute("data-auto-animate-id")&&!(u>r||v>o?m:p).hasAttribute("data-auto-animate-restart")&&(h=!0,K.slides.classList.add("disable-slide-transitions")),V="running"),rt(),He(),ee.isActive()&&ee.update(),void 0!==i&&Z.goto(i),p&&p!==m&&(p.classList.remove("present"),p.setAttribute("aria-hidden","true"),Ve()&&setTimeout((()=>{vt().forEach((e=>{Oe(e,0)}))}),0));e:for(let e=0,t=I.length;e{fe(be(m))})),se.update(),ae.update(),ce.update(),Q.update(),Q.updateParallax(),G.update(),Z.update(),ie.writeURL(),St(),h&&(setTimeout((()=>{K.slides.classList.remove("disable-slide-transitions")}),0),w.autoAnimate&&J.run(p,m))}function Ze(){Ae(),Se(),He(),$=w.autoSlide,St(),Q.create(),ie.writeURL(),Z.sortAll(),ae.update(),se.update(),rt(),ce.update(),ce.updateVisibility(),Q.update(!0),G.update(),_.formatEmbeddedContent(),!1===w.autoPlayMedia?_.stopEmbeddedContent(m,{unloadIframes:!1}):_.startEmbeddedContent(m),ee.isActive()&&ee.layout()}function et(e=m){Q.sync(e),Z.sync(e),_.load(e),Q.update(),ce.update()}function tt(){ut().forEach((e=>{t(e,"section").forEach(((e,t)=>{t>0&&(e.classList.remove("present"),e.classList.remove("past"),e.classList.add("future"),e.setAttribute("aria-hidden","true"))}))}))}function it(e=ut()){e.forEach(((t,i)=>{let a=e[Math.floor(Math.random()*e.length)];a.parentNode===t.parentNode&&t.parentNode.insertBefore(t,a);let s=t.querySelectorAll("section");s.length&&it(s)}))}function at(e,i){let a=t(K.wrapper,e),s=a.length,n=oe.isPrintingPDF(),r=!1,o=!1;if(s){w.loop&&(i>=s&&(r=!0),(i%=s)<0&&(i=s+i,o=!0)),i=Math.max(Math.min(i,s-1),0);for(let e=0;ei?(t.classList.add(s?"past":"future"),w.fragments&&nt(t)):e===i&&w.fragments&&(r?nt(t):o&&st(t))}let e=a[i],t=e.classList.contains("present");e.classList.add("present"),e.removeAttribute("hidden"),e.removeAttribute("aria-hidden"),t||Pe({target:e,type:"visible",bubbles:!1});let l=e.getAttribute("data-state");l&&(I=I.concat(l.split(" ")))}else i=0;return i}function st(e){t(e,".fragment").forEach((e=>{e.classList.add("visible"),e.classList.remove("current-fragment")}))}function nt(e){t(e,".fragment.visible").forEach((e=>{e.classList.remove("visible","current-fragment")}))}function rt(){let e,i,a=ut(),s=a.length;if(s&&void 0!==u){let n=ee.isActive()?10:w.viewDistance;g&&(n=ee.isActive()?6:w.mobileViewDistance),oe.isPrintingPDF()&&(n=Number.MAX_VALUE);for(let r=0;rsection"),i=K.wrapper.querySelectorAll(".slides>section.present>section"),a={left:u>0,right:u0,down:v1&&(a.left=!0,a.right=!0),i.length>1&&(a.up=!0,a.down=!0)),t.length>1&&"linear"===w.navigationMode&&(a.right=a.right||a.down,a.left=a.left||a.up),!0===e){let e=Z.availableRoutes();a.left=a.left||e.prev,a.up=a.up||e.prev,a.down=a.down||e.next,a.right=a.right||e.next}if(w.rtl){let e=a.left;a.left=a.right,a.right=e}return a}function lt(e=m){let t=ut(),i=0;e:for(let a=0;a0){let i=.9;t+=m.querySelectorAll(".fragment.visible").length/e.length*i}}return Math.min(t/(e-1),1)}function ct(e){let i,a=u,s=v;if(e){let i=We(e),n=i?e.parentNode:e,r=ut();a=Math.max(r.indexOf(n),0),s=void 0,i&&(s=Math.max(t(e.parentNode,"section").indexOf(e),0))}if(!e&&m){if(m.querySelectorAll(".fragment").length>0){let e=m.querySelector(".current-fragment");i=e&&e.hasAttribute("data-fragment-index")?parseInt(e.getAttribute("data-fragment-index"),10):m.querySelectorAll(".fragment.visible").length-1}}return{h:a,v:s,f:i}}function ht(){return t(K.wrapper,'.slides section:not(.stack):not([data-visibility="uncounted"])')}function ut(){return t(K.wrapper,".slides>section")}function gt(){return t(K.wrapper,".slides>section>section")}function vt(){return t(K.wrapper,".slides>section.stack")}function pt(){return ut().length>1}function mt(){return gt().length>1}function ft(){return ht().map((e=>{let t={};for(let i=0;i{e.hasAttribute("data-autoplay")&&$&&1e3*e.duration/e.playbackRate>$&&($=1e3*e.duration/e.playbackRate+1e3)}))),!$||Y||_e()||ee.isActive()||$e()&&!Z.availableRoutes().next&&!0!==w.loop||(j=setTimeout((()=>{"function"==typeof w.autoSlideMethod?w.autoSlideMethod():Dt(),St()}),$),X=Date.now()),f&&f.setPlaying(-1!==j)}}function At(){clearTimeout(j),j=-1}function kt(){$&&!Y&&(Y=!0,Pe({type:"autoslidepaused"}),clearTimeout(j),f&&f.setPlaying(!1))}function Lt(){$&&Y&&(Y=!1,Pe({type:"autoslideresumed"}),St())}function Ct({skipFragments:e=!1}={}){A.hasNavigatedHorizontally=!0,w.rtl?(ee.isActive()||e||!1===Z.next())&&ot().left&&Qe(u+1,"grid"===w.navigationMode?v:void 0):(ee.isActive()||e||!1===Z.prev())&&ot().left&&Qe(u-1,"grid"===w.navigationMode?v:void 0)}function xt({skipFragments:e=!1}={}){A.hasNavigatedHorizontally=!0,w.rtl?(ee.isActive()||e||!1===Z.prev())&&ot().right&&Qe(u-1,"grid"===w.navigationMode?v:void 0):(ee.isActive()||e||!1===Z.next())&&ot().right&&Qe(u+1,"grid"===w.navigationMode?v:void 0)}function Pt({skipFragments:e=!1}={}){(ee.isActive()||e||!1===Z.prev())&&ot().up&&Qe(u,v-1)}function Nt({skipFragments:e=!1}={}){A.hasNavigatedVertically=!0,(ee.isActive()||e||!1===Z.next())&&ot().down&&Qe(u,v+1)}function Mt({skipFragments:e=!1}={}){if(e||!1===Z.prev())if(ot().up)Pt({skipFragments:e});else{let i;if(i=w.rtl?t(K.wrapper,".slides>section.future").pop():t(K.wrapper,".slides>section.past").pop(),i&&i.classList.contains("stack")){let e=i.querySelectorAll("section").length-1||void 0;Qe(u-1,e)}else Ct({skipFragments:e})}}function Dt({skipFragments:e=!1}={}){if(A.hasNavigatedHorizontally=!0,A.hasNavigatedVertically=!0,e||!1===Z.next()){let t=ot();t.down&&t.right&&w.loop&&Ke()&&(t.down=!1),t.down?Nt({skipFragments:e}):w.rtl?Ct({skipFragments:e}):xt({skipFragments:e})}}function It(e){w.autoSlideStoppable&&kt()}function Tt(e){let t=e.data;if("string"==typeof t&&"{"===t.charAt(0)&&"}"===t.charAt(t.length-1)&&(t=JSON.parse(t),t.method&&"function"==typeof h[t.method]))if(!1===R.test(t.method)){const e=h[t.method].apply(h,t.args);Ne("callback",{method:t.method,result:e})}else console.warn('reveal.js: "'+t.method+'" is is blacklisted from the postMessage API')}function Ft(e){"running"===V&&/section/gi.test(e.target.nodeName)&&(V="idle",Pe({type:"slidetransitionend",data:{indexh:u,indexv:v,previousSlide:p,currentSlide:m}}))}function zt(e){const t=r(e.target,'a[href^="#"]');if(t){const i=t.getAttribute("href"),a=ie.getIndicesFromHash(i);a&&(h.slide(a.h,a.v,a.f),e.preventDefault())}}function Ht(e){He()}function qt(e){!1===document.hidden&&document.activeElement!==document.body&&("function"==typeof document.activeElement.blur&&document.activeElement.blur(),document.body.focus())}function Bt(e){(document.fullscreenElement||document.webkitFullscreenElement)===K.wrapper&&(e.stopImmediatePropagation(),setTimeout((()=>{h.layout(),h.focus.focus()}),1))}function Ot(e){if(e.currentTarget&&e.currentTarget.hasAttribute("href")){let t=e.currentTarget.getAttribute("href");t&&(Ie(t),e.preventDefault())}}function Ut(e){$e()&&!1===w.loop?(Qe(0,0),Lt()):Y?Lt():kt()}const Wt={VERSION:"4.3.1",initialize:he,configure:Re,destroy:ke,sync:Ze,syncSlide:et,syncFragments:Z.sync.bind(Z),slide:Qe,left:Ct,right:xt,up:Pt,down:Nt,prev:Mt,next:Dt,navigateLeft:Ct,navigateRight:xt,navigateUp:Pt,navigateDown:Nt,navigatePrev:Mt,navigateNext:Dt,navigateFragment:Z.goto.bind(Z),prevFragment:Z.prev.bind(Z),nextFragment:Z.next.bind(Z),on:Le,off:Ce,addEventListener:Le,removeEventListener:Ce,layout:He,shuffle:it,availableRoutes:ot,availableFragments:Z.availableRoutes.bind(Z),toggleHelp:Te,toggleOverview:ee.toggle.bind(ee),togglePause:Ye,toggleAutoSlide:Ge,isFirstSlide:Ve,isLastSlide:$e,isLastVerticalSlide:Ke,isVerticalSlide:We,isPaused:_e,isAutoSliding:Je,isSpeakerNotes:ce.isSpeakerNotesWindow.bind(ce),isOverview:ee.isActive.bind(ee),isFocused:le.isFocused.bind(le),isPrintingPDF:oe.isPrintingPDF.bind(oe),isReady:()=>S,loadSlide:_.load.bind(_),unloadSlide:_.unload.bind(_),showPreview:Ie,hidePreview:ze,addEventListeners:Se,removeEventListeners:Ae,dispatchEvent:Pe,getState:Et,setState:Rt,getProgress:dt,getIndices:ct,getSlidesAttributes:ft,getSlidePastCount:lt,getTotalSlides:bt,getSlide:yt,getPreviousSlide:()=>p,getCurrentSlide:()=>m,getSlideBackground:wt,getSlideNotes:ce.getSlideNotes.bind(ce),getSlides:ht,getHorizontalSlides:ut,getVerticalSlides:gt,hasHorizontalSlides:pt,hasVerticalSlides:mt,hasNavigatedHorizontally:()=>A.hasNavigatedHorizontally,hasNavigatedVertically:()=>A.hasNavigatedVertically,addKeyBinding:te.addKeyBinding.bind(te),removeKeyBinding:te.removeKeyBinding.bind(te),triggerKey:te.triggerKey.bind(te),registerKeyboardShortcut:te.registerKeyboardShortcut.bind(te),getComputedSlideSize:Be,getScale:()=>U,getConfig:()=>w,getQueryHash:d,getSlidePath:ie.getHash.bind(ie),getRevealElement:()=>n,getSlidesElement:()=>K.slides,getViewportElement:()=>K.viewport,getBackgroundsElement:()=>Q.element,registerPlugin:re.registerPlugin.bind(re),hasPlugin:re.hasPlugin.bind(re),getPlugin:re.getPlugin.bind(re),getPlugins:re.getRegisteredPlugins.bind(re)};return e(h,{...Wt,announceStatus:fe,getStatusText:be,print:oe,focus:le,progress:se,controls:ae,location:ie,overview:ee,fragments:Z,slideContent:_,slideNumber:G,onUserInput:It,closeOverlay:ze,updateSlidesVisibility:rt,layoutSlideContents:qe,transformSlides:xe,cueAutoSlide:St,cancelAutoSlide:At}),Wt}let W=U,K=[];W.initialize=e=>(Object.assign(W,new U(document.querySelector(".reveal"),e)),K.map((e=>e(W))),W.initialize()),["configure","on","off","addEventListener","removeEventListener","registerPlugin"].forEach((e=>{W[e]=(...t)=>{K.push((i=>i[e].call(null,...t)))}})),W.isReady=()=>!1,W.VERSION="4.3.1";export default W; +//# sourceMappingURL=reveal.esm.js.map diff --git a/mstreams/presentation/dist/reveal.esm.js.map b/mstreams/presentation/dist/reveal.esm.js.map new file mode 100644 index 0000000..398bebe --- /dev/null +++ b/mstreams/presentation/dist/reveal.esm.js.map @@ -0,0 +1 @@ +{"version":3,"file":"reveal.esm.js","sources":["../js/utils/util.js","../js/utils/device.js","../node_modules/fitty/dist/fitty.module.js","../js/controllers/slidecontent.js","../js/controllers/slidenumber.js","../js/utils/color.js","../js/controllers/backgrounds.js","../js/utils/constants.js","../js/controllers/autoanimate.js","../js/controllers/fragments.js","../js/controllers/overview.js","../js/controllers/keyboard.js","../js/controllers/location.js","../js/controllers/controls.js","../js/controllers/progress.js","../js/controllers/pointer.js","../js/utils/loader.js","../js/controllers/plugins.js","../js/controllers/print.js","../js/controllers/touch.js","../js/controllers/focus.js","../js/controllers/notes.js","../js/components/playback.js","../js/config.js","../js/reveal.js","../js/index.js"],"sourcesContent":["/**\n * Extend object a with the properties of object b.\n * If there's a conflict, object b takes precedence.\n *\n * @param {object} a\n * @param {object} b\n */\nexport const extend = ( a, b ) => {\n\n\tfor( let i in b ) {\n\t\ta[ i ] = b[ i ];\n\t}\n\n\treturn a;\n\n}\n\n/**\n * querySelectorAll but returns an Array.\n */\nexport const queryAll = ( el, selector ) => {\n\n\treturn Array.from( el.querySelectorAll( selector ) );\n\n}\n\n/**\n * classList.toggle() with cross browser support\n */\nexport const toggleClass = ( el, className, value ) => {\n\tif( value ) {\n\t\tel.classList.add( className );\n\t}\n\telse {\n\t\tel.classList.remove( className );\n\t}\n}\n\n/**\n * Utility for deserializing a value.\n *\n * @param {*} value\n * @return {*}\n */\nexport const deserialize = ( value ) => {\n\n\tif( typeof value === 'string' ) {\n\t\tif( value === 'null' ) return null;\n\t\telse if( value === 'true' ) return true;\n\t\telse if( value === 'false' ) return false;\n\t\telse if( value.match( /^-?[\\d\\.]+$/ ) ) return parseFloat( value );\n\t}\n\n\treturn value;\n\n}\n\n/**\n * Measures the distance in pixels between point a\n * and point b.\n *\n * @param {object} a point with x/y properties\n * @param {object} b point with x/y properties\n *\n * @return {number}\n */\nexport const distanceBetween = ( a, b ) => {\n\n\tlet dx = a.x - b.x,\n\t\tdy = a.y - b.y;\n\n\treturn Math.sqrt( dx*dx + dy*dy );\n\n}\n\n/**\n * Applies a CSS transform to the target element.\n *\n * @param {HTMLElement} element\n * @param {string} transform\n */\nexport const transformElement = ( element, transform ) => {\n\n\telement.style.transform = transform;\n\n}\n\n/**\n * Element.matches with IE support.\n *\n * @param {HTMLElement} target The element to match\n * @param {String} selector The CSS selector to match\n * the element against\n *\n * @return {Boolean}\n */\nexport const matches = ( target, selector ) => {\n\n\tlet matchesMethod = target.matches || target.matchesSelector || target.msMatchesSelector;\n\n\treturn !!( matchesMethod && matchesMethod.call( target, selector ) );\n\n}\n\n/**\n * Find the closest parent that matches the given\n * selector.\n *\n * @param {HTMLElement} target The child element\n * @param {String} selector The CSS selector to match\n * the parents against\n *\n * @return {HTMLElement} The matched parent or null\n * if no matching parent was found\n */\nexport const closest = ( target, selector ) => {\n\n\t// Native Element.closest\n\tif( typeof target.closest === 'function' ) {\n\t\treturn target.closest( selector );\n\t}\n\n\t// Polyfill\n\twhile( target ) {\n\t\tif( matches( target, selector ) ) {\n\t\t\treturn target;\n\t\t}\n\n\t\t// Keep searching\n\t\ttarget = target.parentNode;\n\t}\n\n\treturn null;\n\n}\n\n/**\n * Handling the fullscreen functionality via the fullscreen API\n *\n * @see http://fullscreen.spec.whatwg.org/\n * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode\n */\nexport const enterFullscreen = element => {\n\n\telement = element || document.documentElement;\n\n\t// Check which implementation is available\n\tlet requestMethod = element.requestFullscreen ||\n\t\t\t\t\t\telement.webkitRequestFullscreen ||\n\t\t\t\t\t\telement.webkitRequestFullScreen ||\n\t\t\t\t\t\telement.mozRequestFullScreen ||\n\t\t\t\t\t\telement.msRequestFullscreen;\n\n\tif( requestMethod ) {\n\t\trequestMethod.apply( element );\n\t}\n\n}\n\n/**\n * Creates an HTML element and returns a reference to it.\n * If the element already exists the existing instance will\n * be returned.\n *\n * @param {HTMLElement} container\n * @param {string} tagname\n * @param {string} classname\n * @param {string} innerHTML\n *\n * @return {HTMLElement}\n */\nexport const createSingletonNode = ( container, tagname, classname, innerHTML='' ) => {\n\n\t// Find all nodes matching the description\n\tlet nodes = container.querySelectorAll( '.' + classname );\n\n\t// Check all matches to find one which is a direct child of\n\t// the specified container\n\tfor( let i = 0; i < nodes.length; i++ ) {\n\t\tlet testNode = nodes[i];\n\t\tif( testNode.parentNode === container ) {\n\t\t\treturn testNode;\n\t\t}\n\t}\n\n\t// If no node was found, create it now\n\tlet node = document.createElement( tagname );\n\tnode.className = classname;\n\tnode.innerHTML = innerHTML;\n\tcontainer.appendChild( node );\n\n\treturn node;\n\n}\n\n/**\n * Injects the given CSS styles into the DOM.\n *\n * @param {string} value\n */\nexport const createStyleSheet = ( value ) => {\n\n\tlet tag = document.createElement( 'style' );\n\ttag.type = 'text/css';\n\n\tif( value && value.length > 0 ) {\n\t\tif( tag.styleSheet ) {\n\t\t\ttag.styleSheet.cssText = value;\n\t\t}\n\t\telse {\n\t\t\ttag.appendChild( document.createTextNode( value ) );\n\t\t}\n\t}\n\n\tdocument.head.appendChild( tag );\n\n\treturn tag;\n\n}\n\n/**\n * Returns a key:value hash of all query params.\n */\nexport const getQueryHash = () => {\n\n\tlet query = {};\n\n\tlocation.search.replace( /[A-Z0-9]+?=([\\w\\.%-]*)/gi, a => {\n\t\tquery[ a.split( '=' ).shift() ] = a.split( '=' ).pop();\n\t} );\n\n\t// Basic deserialization\n\tfor( let i in query ) {\n\t\tlet value = query[ i ];\n\n\t\tquery[ i ] = deserialize( unescape( value ) );\n\t}\n\n\t// Do not accept new dependencies via query config to avoid\n\t// the potential of malicious script injection\n\tif( typeof query['dependencies'] !== 'undefined' ) delete query['dependencies'];\n\n\treturn query;\n\n}\n\n/**\n * Returns the remaining height within the parent of the\n * target element.\n *\n * remaining height = [ configured parent height ] - [ current parent height ]\n *\n * @param {HTMLElement} element\n * @param {number} [height]\n */\nexport const getRemainingHeight = ( element, height = 0 ) => {\n\n\tif( element ) {\n\t\tlet newHeight, oldHeight = element.style.height;\n\n\t\t// Change the .stretch element height to 0 in order find the height of all\n\t\t// the other elements\n\t\telement.style.height = '0px';\n\n\t\t// In Overview mode, the parent (.slide) height is set of 700px.\n\t\t// Restore it temporarily to its natural height.\n\t\telement.parentNode.style.height = 'auto';\n\n\t\tnewHeight = height - element.parentNode.offsetHeight;\n\n\t\t// Restore the old height, just in case\n\t\telement.style.height = oldHeight + 'px';\n\n\t\t// Clear the parent (.slide) height. .removeProperty works in IE9+\n\t\telement.parentNode.style.removeProperty('height');\n\n\t\treturn newHeight;\n\t}\n\n\treturn height;\n\n}\n\nconst fileExtensionToMimeMap = {\n\t'mp4': 'video/mp4',\n\t'm4a': 'video/mp4',\n\t'ogv': 'video/ogg',\n\t'mpeg': 'video/mpeg',\n\t'webm': 'video/webm'\n}\n\n/**\n * Guess the MIME type for common file formats.\n */\nexport const getMimeTypeFromFile = ( filename='' ) => {\n\treturn fileExtensionToMimeMap[filename.split('.').pop()]\n}","const UA = navigator.userAgent;\n\nexport const isMobile = /(iphone|ipod|ipad|android)/gi.test( UA ) ||\n\t\t\t\t\t\t( navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1 ); // iPadOS\n\nexport const isChrome = /chrome/i.test( UA ) && !/edge/i.test( UA );\n\nexport const isAndroid = /android/gi.test( UA );","/*\n * fitty v2.3.3 - Snugly resizes text to fit its parent container\n * Copyright (c) 2020 Rik Schennink (https://pqina.nl/)\n */\n'use strict';\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };\n\nexports.default = function (w) {\n\n // no window, early exit\n if (!w) return;\n\n // node list to array helper method\n var toArray = function toArray(nl) {\n return [].slice.call(nl);\n };\n\n // states\n var DrawState = {\n IDLE: 0,\n DIRTY_CONTENT: 1,\n DIRTY_LAYOUT: 2,\n DIRTY: 3\n };\n\n // all active fitty elements\n var fitties = [];\n\n // group all redraw calls till next frame, we cancel each frame request when a new one comes in. If no support for request animation frame, this is an empty function and supports for fitty stops.\n var redrawFrame = null;\n var requestRedraw = 'requestAnimationFrame' in w ? function () {\n w.cancelAnimationFrame(redrawFrame);\n redrawFrame = w.requestAnimationFrame(function () {\n return redraw(fitties.filter(function (f) {\n return f.dirty && f.active;\n }));\n });\n } : function () {};\n\n // sets all fitties to dirty so they are redrawn on the next redraw loop, then calls redraw\n var redrawAll = function redrawAll(type) {\n return function () {\n fitties.forEach(function (f) {\n return f.dirty = type;\n });\n requestRedraw();\n };\n };\n\n // redraws fitties so they nicely fit their parent container\n var redraw = function redraw(fitties) {\n\n // getting info from the DOM at this point should not trigger a reflow, let's gather as much intel as possible before triggering a reflow\n\n // check if styles of all fitties have been computed\n fitties.filter(function (f) {\n return !f.styleComputed;\n }).forEach(function (f) {\n f.styleComputed = computeStyle(f);\n });\n\n // restyle elements that require pre-styling, this triggers a reflow, please try to prevent by adding CSS rules (see docs)\n fitties.filter(shouldPreStyle).forEach(applyStyle);\n\n // we now determine which fitties should be redrawn\n var fittiesToRedraw = fitties.filter(shouldRedraw);\n\n // we calculate final styles for these fitties\n fittiesToRedraw.forEach(calculateStyles);\n\n // now we apply the calculated styles from our previous loop\n fittiesToRedraw.forEach(function (f) {\n applyStyle(f);\n markAsClean(f);\n });\n\n // now we dispatch events for all restyled fitties\n fittiesToRedraw.forEach(dispatchFitEvent);\n };\n\n var markAsClean = function markAsClean(f) {\n return f.dirty = DrawState.IDLE;\n };\n\n var calculateStyles = function calculateStyles(f) {\n\n // get available width from parent node\n f.availableWidth = f.element.parentNode.clientWidth;\n\n // the space our target element uses\n f.currentWidth = f.element.scrollWidth;\n\n // remember current font size\n f.previousFontSize = f.currentFontSize;\n\n // let's calculate the new font size\n f.currentFontSize = Math.min(Math.max(f.minSize, f.availableWidth / f.currentWidth * f.previousFontSize), f.maxSize);\n\n // if allows wrapping, only wrap when at minimum font size (otherwise would break container)\n f.whiteSpace = f.multiLine && f.currentFontSize === f.minSize ? 'normal' : 'nowrap';\n };\n\n // should always redraw if is not dirty layout, if is dirty layout, only redraw if size has changed\n var shouldRedraw = function shouldRedraw(f) {\n return f.dirty !== DrawState.DIRTY_LAYOUT || f.dirty === DrawState.DIRTY_LAYOUT && f.element.parentNode.clientWidth !== f.availableWidth;\n };\n\n // every fitty element is tested for invalid styles\n var computeStyle = function computeStyle(f) {\n\n // get style properties\n var style = w.getComputedStyle(f.element, null);\n\n // get current font size in pixels (if we already calculated it, use the calculated version)\n f.currentFontSize = parseFloat(style.getPropertyValue('font-size'));\n\n // get display type and wrap mode\n f.display = style.getPropertyValue('display');\n f.whiteSpace = style.getPropertyValue('white-space');\n };\n\n // determines if this fitty requires initial styling, can be prevented by applying correct styles through CSS\n var shouldPreStyle = function shouldPreStyle(f) {\n\n var preStyle = false;\n\n // if we already tested for prestyling we don't have to do it again\n if (f.preStyleTestCompleted) return false;\n\n // should have an inline style, if not, apply\n if (!/inline-/.test(f.display)) {\n preStyle = true;\n f.display = 'inline-block';\n }\n\n // to correctly calculate dimensions the element should have whiteSpace set to nowrap\n if (f.whiteSpace !== 'nowrap') {\n preStyle = true;\n f.whiteSpace = 'nowrap';\n }\n\n // we don't have to do this twice\n f.preStyleTestCompleted = true;\n\n return preStyle;\n };\n\n // apply styles to single fitty\n var applyStyle = function applyStyle(f) {\n f.element.style.whiteSpace = f.whiteSpace;\n f.element.style.display = f.display;\n f.element.style.fontSize = f.currentFontSize + 'px';\n };\n\n // dispatch a fit event on a fitty\n var dispatchFitEvent = function dispatchFitEvent(f) {\n f.element.dispatchEvent(new CustomEvent('fit', {\n detail: {\n oldValue: f.previousFontSize,\n newValue: f.currentFontSize,\n scaleFactor: f.currentFontSize / f.previousFontSize\n }\n }));\n };\n\n // fit method, marks the fitty as dirty and requests a redraw (this will also redraw any other fitty marked as dirty)\n var fit = function fit(f, type) {\n return function () {\n f.dirty = type;\n if (!f.active) return;\n requestRedraw();\n };\n };\n\n var init = function init(f) {\n\n // save some of the original CSS properties before we change them\n f.originalStyle = {\n whiteSpace: f.element.style.whiteSpace,\n display: f.element.style.display,\n fontSize: f.element.style.fontSize\n };\n\n // should we observe DOM mutations\n observeMutations(f);\n\n // this is a new fitty so we need to validate if it's styles are in order\n f.newbie = true;\n\n // because it's a new fitty it should also be dirty, we want it to redraw on the first loop\n f.dirty = true;\n\n // we want to be able to update this fitty\n fitties.push(f);\n };\n\n var destroy = function destroy(f) {\n return function () {\n\n // remove from fitties array\n fitties = fitties.filter(function (_) {\n return _.element !== f.element;\n });\n\n // stop observing DOM\n if (f.observeMutations) f.observer.disconnect();\n\n // reset the CSS properties we changes\n f.element.style.whiteSpace = f.originalStyle.whiteSpace;\n f.element.style.display = f.originalStyle.display;\n f.element.style.fontSize = f.originalStyle.fontSize;\n };\n };\n\n // add a new fitty, does not redraw said fitty\n var subscribe = function subscribe(f) {\n return function () {\n if (f.active) return;\n f.active = true;\n requestRedraw();\n };\n };\n\n // remove an existing fitty\n var unsubscribe = function unsubscribe(f) {\n return function () {\n return f.active = false;\n };\n };\n\n var observeMutations = function observeMutations(f) {\n\n // no observing?\n if (!f.observeMutations) return;\n\n // start observing mutations\n f.observer = new MutationObserver(fit(f, DrawState.DIRTY_CONTENT));\n\n // start observing\n f.observer.observe(f.element, f.observeMutations);\n };\n\n // default mutation observer settings\n var mutationObserverDefaultSetting = {\n subtree: true,\n childList: true,\n characterData: true\n };\n\n // default fitty options\n var defaultOptions = {\n minSize: 16,\n maxSize: 512,\n multiLine: true,\n observeMutations: 'MutationObserver' in w ? mutationObserverDefaultSetting : false\n };\n\n // array of elements in, fitty instances out\n function fittyCreate(elements, options) {\n\n // set options object\n var fittyOptions = _extends({}, defaultOptions, options);\n\n // create fitties\n var publicFitties = elements.map(function (element) {\n\n // create fitty instance\n var f = _extends({}, fittyOptions, {\n\n // internal options for this fitty\n element: element,\n active: true\n });\n\n // initialise this fitty\n init(f);\n\n // expose API\n return {\n element: element,\n fit: fit(f, DrawState.DIRTY),\n unfreeze: subscribe(f),\n freeze: unsubscribe(f),\n unsubscribe: destroy(f)\n };\n });\n\n // call redraw on newly initiated fitties\n requestRedraw();\n\n // expose fitties\n return publicFitties;\n }\n\n // fitty creation function\n function fitty(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};\n\n\n // if target is a string\n return typeof target === 'string' ?\n\n // treat it as a querySelector\n fittyCreate(toArray(document.querySelectorAll(target)), options) :\n\n // create single fitty\n fittyCreate([target], options)[0];\n }\n\n // handles viewport changes, redraws all fitties, but only does so after a timeout\n var resizeDebounce = null;\n var onWindowResized = function onWindowResized() {\n w.clearTimeout(resizeDebounce);\n resizeDebounce = w.setTimeout(redrawAll(DrawState.DIRTY_LAYOUT), fitty.observeWindowDelay);\n };\n\n // define observe window property, so when we set it to true or false events are automatically added and removed\n var events = ['resize', 'orientationchange'];\n Object.defineProperty(fitty, 'observeWindow', {\n set: function set(enabled) {\n var method = (enabled ? 'add' : 'remove') + 'EventListener';\n events.forEach(function (e) {\n w[method](e, onWindowResized);\n });\n }\n });\n\n // fitty global properties (by setting observeWindow to true the events above get added)\n fitty.observeWindow = true;\n fitty.observeWindowDelay = 100;\n\n // public fit all method, will force redraw no matter what\n fitty.fitAll = redrawAll(DrawState.DIRTY);\n\n // export our fitty function, we don't want to keep it to our selves\n return fitty;\n}(typeof window === 'undefined' ? null : window);","import { extend, queryAll, closest, getMimeTypeFromFile } from '../utils/util.js'\nimport { isMobile } from '../utils/device.js'\n\nimport fitty from 'fitty';\n\n/**\n * Handles loading, unloading and playback of slide\n * content such as images, videos and iframes.\n */\nexport default class SlideContent {\n\n\tconstructor( Reveal ) {\n\n\t\tthis.Reveal = Reveal;\n\n\t\tthis.startEmbeddedIframe = this.startEmbeddedIframe.bind( this );\n\n\t}\n\n\t/**\n\t * Should the given element be preloaded?\n\t * Decides based on local element attributes and global config.\n\t *\n\t * @param {HTMLElement} element\n\t */\n\tshouldPreload( element ) {\n\n\t\t// Prefer an explicit global preload setting\n\t\tlet preload = this.Reveal.getConfig().preloadIframes;\n\n\t\t// If no global setting is available, fall back on the element's\n\t\t// own preload setting\n\t\tif( typeof preload !== 'boolean' ) {\n\t\t\tpreload = element.hasAttribute( 'data-preload' );\n\t\t}\n\n\t\treturn preload;\n\t}\n\n\t/**\n\t * Called when the given slide is within the configured view\n\t * distance. Shows the slide element and loads any content\n\t * that is set to load lazily (data-src).\n\t *\n\t * @param {HTMLElement} slide Slide to show\n\t */\n\tload( slide, options = {} ) {\n\n\t\t// Show the slide element\n\t\tslide.style.display = this.Reveal.getConfig().display;\n\n\t\t// Media elements with data-src attributes\n\t\tqueryAll( slide, 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ).forEach( element => {\n\t\t\tif( element.tagName !== 'IFRAME' || this.shouldPreload( element ) ) {\n\t\t\t\telement.setAttribute( 'src', element.getAttribute( 'data-src' ) );\n\t\t\t\telement.setAttribute( 'data-lazy-loaded', '' );\n\t\t\t\telement.removeAttribute( 'data-src' );\n\t\t\t}\n\t\t} );\n\n\t\t// Media elements with children\n\t\tqueryAll( slide, 'video, audio' ).forEach( media => {\n\t\t\tlet sources = 0;\n\n\t\t\tqueryAll( media, 'source[data-src]' ).forEach( source => {\n\t\t\t\tsource.setAttribute( 'src', source.getAttribute( 'data-src' ) );\n\t\t\t\tsource.removeAttribute( 'data-src' );\n\t\t\t\tsource.setAttribute( 'data-lazy-loaded', '' );\n\t\t\t\tsources += 1;\n\t\t\t} );\n\n\t\t\t// Enable inline video playback in mobile Safari\n\t\t\tif( isMobile && media.tagName === 'VIDEO' ) {\n\t\t\t\tmedia.setAttribute( 'playsinline', '' );\n\t\t\t}\n\n\t\t\t// If we rewrote sources for this video/audio element, we need\n\t\t\t// to manually tell it to load from its new origin\n\t\t\tif( sources > 0 ) {\n\t\t\t\tmedia.load();\n\t\t\t}\n\t\t} );\n\n\n\t\t// Show the corresponding background element\n\t\tlet background = slide.slideBackgroundElement;\n\t\tif( background ) {\n\t\t\tbackground.style.display = 'block';\n\n\t\t\tlet backgroundContent = slide.slideBackgroundContentElement;\n\t\t\tlet backgroundIframe = slide.getAttribute( 'data-background-iframe' );\n\n\t\t\t// If the background contains media, load it\n\t\t\tif( background.hasAttribute( 'data-loaded' ) === false ) {\n\t\t\t\tbackground.setAttribute( 'data-loaded', 'true' );\n\n\t\t\t\tlet backgroundImage = slide.getAttribute( 'data-background-image' ),\n\t\t\t\t\tbackgroundVideo = slide.getAttribute( 'data-background-video' ),\n\t\t\t\t\tbackgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),\n\t\t\t\t\tbackgroundVideoMuted = slide.hasAttribute( 'data-background-video-muted' );\n\n\t\t\t\t// Images\n\t\t\t\tif( backgroundImage ) {\n\t\t\t\t\t// base64\n\t\t\t\t\tif( /^data:/.test( backgroundImage.trim() ) ) {\n\t\t\t\t\t\tbackgroundContent.style.backgroundImage = `url(${backgroundImage.trim()})`;\n\t\t\t\t\t}\n\t\t\t\t\t// URL(s)\n\t\t\t\t\telse {\n\t\t\t\t\t\tbackgroundContent.style.backgroundImage = backgroundImage.split( ',' ).map( background => {\n\t\t\t\t\t\t\treturn `url(${encodeURI(background.trim())})`;\n\t\t\t\t\t\t}).join( ',' );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Videos\n\t\t\t\telse if ( backgroundVideo && !this.Reveal.isSpeakerNotes() ) {\n\t\t\t\t\tlet video = document.createElement( 'video' );\n\n\t\t\t\t\tif( backgroundVideoLoop ) {\n\t\t\t\t\t\tvideo.setAttribute( 'loop', '' );\n\t\t\t\t\t}\n\n\t\t\t\t\tif( backgroundVideoMuted ) {\n\t\t\t\t\t\tvideo.muted = true;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Enable inline playback in mobile Safari\n\t\t\t\t\t//\n\t\t\t\t\t// Mute is required for video to play when using\n\t\t\t\t\t// swipe gestures to navigate since they don't\n\t\t\t\t\t// count as direct user actions :'(\n\t\t\t\t\tif( isMobile ) {\n\t\t\t\t\t\tvideo.muted = true;\n\t\t\t\t\t\tvideo.setAttribute( 'playsinline', '' );\n\t\t\t\t\t}\n\n\t\t\t\t\t// Support comma separated lists of video sources\n\t\t\t\t\tbackgroundVideo.split( ',' ).forEach( source => {\n\t\t\t\t\t\tlet type = getMimeTypeFromFile( source );\n\t\t\t\t\t\tif( type ) {\n\t\t\t\t\t\t\tvideo.innerHTML += ``;\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tvideo.innerHTML += ``;\n\t\t\t\t\t\t}\n\t\t\t\t\t} );\n\n\t\t\t\t\tbackgroundContent.appendChild( video );\n\t\t\t\t}\n\t\t\t\t// Iframes\n\t\t\t\telse if( backgroundIframe && options.excludeIframes !== true ) {\n\t\t\t\t\tlet iframe = document.createElement( 'iframe' );\n\t\t\t\t\tiframe.setAttribute( 'allowfullscreen', '' );\n\t\t\t\t\tiframe.setAttribute( 'mozallowfullscreen', '' );\n\t\t\t\t\tiframe.setAttribute( 'webkitallowfullscreen', '' );\n\t\t\t\t\tiframe.setAttribute( 'allow', 'autoplay' );\n\n\t\t\t\t\tiframe.setAttribute( 'data-src', backgroundIframe );\n\n\t\t\t\t\tiframe.style.width = '100%';\n\t\t\t\t\tiframe.style.height = '100%';\n\t\t\t\t\tiframe.style.maxHeight = '100%';\n\t\t\t\t\tiframe.style.maxWidth = '100%';\n\n\t\t\t\t\tbackgroundContent.appendChild( iframe );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Start loading preloadable iframes\n\t\t\tlet backgroundIframeElement = backgroundContent.querySelector( 'iframe[data-src]' );\n\t\t\tif( backgroundIframeElement ) {\n\n\t\t\t\t// Check if this iframe is eligible to be preloaded\n\t\t\t\tif( this.shouldPreload( background ) && !/autoplay=(1|true|yes)/gi.test( backgroundIframe ) ) {\n\t\t\t\t\tif( backgroundIframeElement.getAttribute( 'src' ) !== backgroundIframe ) {\n\t\t\t\t\t\tbackgroundIframeElement.setAttribute( 'src', backgroundIframe );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tthis.layout( slide );\n\n\t}\n\n\t/**\n\t * Applies JS-dependent layout helpers for the scope.\n\t */\n\tlayout( scopeElement ) {\n\n\t\t// Autosize text with the r-fit-text class based on the\n\t\t// size of its container. This needs to happen after the\n\t\t// slide is visible in order to measure the text.\n\t\tArray.from( scopeElement.querySelectorAll( '.r-fit-text' ) ).forEach( element => {\n\t\t\tfitty( element, {\n\t\t\t\tminSize: 24,\n\t\t\t\tmaxSize: this.Reveal.getConfig().height * 0.8,\n\t\t\t\tobserveMutations: false,\n\t\t\t\tobserveWindow: false\n\t\t\t} );\n\t\t} );\n\n\t}\n\n\t/**\n\t * Unloads and hides the given slide. This is called when the\n\t * slide is moved outside of the configured view distance.\n\t *\n\t * @param {HTMLElement} slide\n\t */\n\tunload( slide ) {\n\n\t\t// Hide the slide element\n\t\tslide.style.display = 'none';\n\n\t\t// Hide the corresponding background element\n\t\tlet background = this.Reveal.getSlideBackground( slide );\n\t\tif( background ) {\n\t\t\tbackground.style.display = 'none';\n\n\t\t\t// Unload any background iframes\n\t\t\tqueryAll( background, 'iframe[src]' ).forEach( element => {\n\t\t\t\telement.removeAttribute( 'src' );\n\t\t\t} );\n\t\t}\n\n\t\t// Reset lazy-loaded media elements with src attributes\n\t\tqueryAll( slide, 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]' ).forEach( element => {\n\t\t\telement.setAttribute( 'data-src', element.getAttribute( 'src' ) );\n\t\t\telement.removeAttribute( 'src' );\n\t\t} );\n\n\t\t// Reset lazy-loaded media elements with children\n\t\tqueryAll( slide, 'video[data-lazy-loaded] source[src], audio source[src]' ).forEach( source => {\n\t\t\tsource.setAttribute( 'data-src', source.getAttribute( 'src' ) );\n\t\t\tsource.removeAttribute( 'src' );\n\t\t} );\n\n\t}\n\n\t/**\n\t * Enforces origin-specific format rules for embedded media.\n\t */\n\tformatEmbeddedContent() {\n\n\t\tlet _appendParamToIframeSource = ( sourceAttribute, sourceURL, param ) => {\n\t\t\tqueryAll( this.Reveal.getSlidesElement(), 'iframe['+ sourceAttribute +'*=\"'+ sourceURL +'\"]' ).forEach( el => {\n\t\t\t\tlet src = el.getAttribute( sourceAttribute );\n\t\t\t\tif( src && src.indexOf( param ) === -1 ) {\n\t\t\t\t\tel.setAttribute( sourceAttribute, src + ( !/\\?/.test( src ) ? '?' : '&' ) + param );\n\t\t\t\t}\n\t\t\t});\n\t\t};\n\n\t\t// YouTube frames must include \"?enablejsapi=1\"\n\t\t_appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' );\n\t\t_appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' );\n\n\t\t// Vimeo frames must include \"?api=1\"\n\t\t_appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );\n\t\t_appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );\n\n\t}\n\n\t/**\n\t * Start playback of any embedded content inside of\n\t * the given element.\n\t *\n\t * @param {HTMLElement} element\n\t */\n\tstartEmbeddedContent( element ) {\n\n\t\tif( element && !this.Reveal.isSpeakerNotes() ) {\n\n\t\t\t// Restart GIFs\n\t\t\tqueryAll( element, 'img[src$=\".gif\"]' ).forEach( el => {\n\t\t\t\t// Setting the same unchanged source like this was confirmed\n\t\t\t\t// to work in Chrome, FF & Safari\n\t\t\t\tel.setAttribute( 'src', el.getAttribute( 'src' ) );\n\t\t\t} );\n\n\t\t\t// HTML5 media elements\n\t\t\tqueryAll( element, 'video, audio' ).forEach( el => {\n\t\t\t\tif( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Prefer an explicit global autoplay setting\n\t\t\t\tlet autoplay = this.Reveal.getConfig().autoPlayMedia;\n\n\t\t\t\t// If no global setting is available, fall back on the element's\n\t\t\t\t// own autoplay setting\n\t\t\t\tif( typeof autoplay !== 'boolean' ) {\n\t\t\t\t\tautoplay = el.hasAttribute( 'data-autoplay' ) || !!closest( el, '.slide-background' );\n\t\t\t\t}\n\n\t\t\t\tif( autoplay && typeof el.play === 'function' ) {\n\n\t\t\t\t\t// If the media is ready, start playback\n\t\t\t\t\tif( el.readyState > 1 ) {\n\t\t\t\t\t\tthis.startEmbeddedMedia( { target: el } );\n\t\t\t\t\t}\n\t\t\t\t\t// Mobile devices never fire a loaded event so instead\n\t\t\t\t\t// of waiting, we initiate playback\n\t\t\t\t\telse if( isMobile ) {\n\t\t\t\t\t\tlet promise = el.play();\n\n\t\t\t\t\t\t// If autoplay does not work, ensure that the controls are visible so\n\t\t\t\t\t\t// that the viewer can start the media on their own\n\t\t\t\t\t\tif( promise && typeof promise.catch === 'function' && el.controls === false ) {\n\t\t\t\t\t\t\tpromise.catch( () => {\n\t\t\t\t\t\t\t\tel.controls = true;\n\n\t\t\t\t\t\t\t\t// Once the video does start playing, hide the controls again\n\t\t\t\t\t\t\t\tel.addEventListener( 'play', () => {\n\t\t\t\t\t\t\t\t\tel.controls = false;\n\t\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// If the media isn't loaded, wait before playing\n\t\t\t\t\telse {\n\t\t\t\t\t\tel.removeEventListener( 'loadeddata', this.startEmbeddedMedia ); // remove first to avoid dupes\n\t\t\t\t\t\tel.addEventListener( 'loadeddata', this.startEmbeddedMedia );\n\t\t\t\t\t}\n\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\t// Normal iframes\n\t\t\tqueryAll( element, 'iframe[src]' ).forEach( el => {\n\t\t\t\tif( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tthis.startEmbeddedIframe( { target: el } );\n\t\t\t} );\n\n\t\t\t// Lazy loading iframes\n\t\t\tqueryAll( element, 'iframe[data-src]' ).forEach( el => {\n\t\t\t\tif( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {\n\t\t\t\t\tel.removeEventListener( 'load', this.startEmbeddedIframe ); // remove first to avoid dupes\n\t\t\t\t\tel.addEventListener( 'load', this.startEmbeddedIframe );\n\t\t\t\t\tel.setAttribute( 'src', el.getAttribute( 'data-src' ) );\n\t\t\t\t}\n\t\t\t} );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Starts playing an embedded video/audio element after\n\t * it has finished loading.\n\t *\n\t * @param {object} event\n\t */\n\tstartEmbeddedMedia( event ) {\n\n\t\tlet isAttachedToDOM = !!closest( event.target, 'html' ),\n\t\t\tisVisible \t\t= !!closest( event.target, '.present' );\n\n\t\tif( isAttachedToDOM && isVisible ) {\n\t\t\tevent.target.currentTime = 0;\n\t\t\tevent.target.play();\n\t\t}\n\n\t\tevent.target.removeEventListener( 'loadeddata', this.startEmbeddedMedia );\n\n\t}\n\n\t/**\n\t * \"Starts\" the content of an embedded iframe using the\n\t * postMessage API.\n\t *\n\t * @param {object} event\n\t */\n\tstartEmbeddedIframe( event ) {\n\n\t\tlet iframe = event.target;\n\n\t\tif( iframe && iframe.contentWindow ) {\n\n\t\t\tlet isAttachedToDOM = !!closest( event.target, 'html' ),\n\t\t\t\tisVisible \t\t= !!closest( event.target, '.present' );\n\n\t\t\tif( isAttachedToDOM && isVisible ) {\n\n\t\t\t\t// Prefer an explicit global autoplay setting\n\t\t\t\tlet autoplay = this.Reveal.getConfig().autoPlayMedia;\n\n\t\t\t\t// If no global setting is available, fall back on the element's\n\t\t\t\t// own autoplay setting\n\t\t\t\tif( typeof autoplay !== 'boolean' ) {\n\t\t\t\t\tautoplay = iframe.hasAttribute( 'data-autoplay' ) || !!closest( iframe, '.slide-background' );\n\t\t\t\t}\n\n\t\t\t\t// YouTube postMessage API\n\t\t\t\tif( /youtube\\.com\\/embed\\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {\n\t\t\t\t\tiframe.contentWindow.postMessage( '{\"event\":\"command\",\"func\":\"playVideo\",\"args\":\"\"}', '*' );\n\t\t\t\t}\n\t\t\t\t// Vimeo postMessage API\n\t\t\t\telse if( /player\\.vimeo\\.com\\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {\n\t\t\t\t\tiframe.contentWindow.postMessage( '{\"method\":\"play\"}', '*' );\n\t\t\t\t}\n\t\t\t\t// Generic postMessage API\n\t\t\t\telse {\n\t\t\t\t\tiframe.contentWindow.postMessage( 'slide:start', '*' );\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Stop playback of any embedded content inside of\n\t * the targeted slide.\n\t *\n\t * @param {HTMLElement} element\n\t */\n\tstopEmbeddedContent( element, options = {} ) {\n\n\t\toptions = extend( {\n\t\t\t// Defaults\n\t\t\tunloadIframes: true\n\t\t}, options );\n\n\t\tif( element && element.parentNode ) {\n\t\t\t// HTML5 media elements\n\t\t\tqueryAll( element, 'video, audio' ).forEach( el => {\n\t\t\t\tif( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {\n\t\t\t\t\tel.setAttribute('data-paused-by-reveal', '');\n\t\t\t\t\tel.pause();\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\t// Generic postMessage API for non-lazy loaded iframes\n\t\t\tqueryAll( element, 'iframe' ).forEach( el => {\n\t\t\t\tif( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' );\n\t\t\t\tel.removeEventListener( 'load', this.startEmbeddedIframe );\n\t\t\t});\n\n\t\t\t// YouTube postMessage API\n\t\t\tqueryAll( element, 'iframe[src*=\"youtube.com/embed/\"]' ).forEach( el => {\n\t\t\t\tif( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {\n\t\t\t\t\tel.contentWindow.postMessage( '{\"event\":\"command\",\"func\":\"pauseVideo\",\"args\":\"\"}', '*' );\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Vimeo postMessage API\n\t\t\tqueryAll( element, 'iframe[src*=\"player.vimeo.com/\"]' ).forEach( el => {\n\t\t\t\tif( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {\n\t\t\t\t\tel.contentWindow.postMessage( '{\"method\":\"pause\"}', '*' );\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tif( options.unloadIframes === true ) {\n\t\t\t\t// Unload lazy-loaded iframes\n\t\t\t\tqueryAll( element, 'iframe[data-src]' ).forEach( el => {\n\t\t\t\t\t// Only removing the src doesn't actually unload the frame\n\t\t\t\t\t// in all browsers (Firefox) so we set it to blank first\n\t\t\t\t\tel.setAttribute( 'src', 'about:blank' );\n\t\t\t\t\tel.removeAttribute( 'src' );\n\t\t\t\t} );\n\t\t\t}\n\t\t}\n\n\t}\n\n}\n","/**\n * Handles the display of reveal.js' optional slide number.\n */\nexport default class SlideNumber {\n\n\tconstructor( Reveal ) {\n\n\t\tthis.Reveal = Reveal;\n\n\t}\n\n\trender() {\n\n\t\tthis.element = document.createElement( 'div' );\n\t\tthis.element.className = 'slide-number';\n\t\tthis.Reveal.getRevealElement().appendChild( this.element );\n\n\t}\n\n\t/**\n\t * Called when the reveal.js config is updated.\n\t */\n\tconfigure( config, oldConfig ) {\n\n\t\tlet slideNumberDisplay = 'none';\n\t\tif( config.slideNumber && !this.Reveal.isPrintingPDF() ) {\n\t\t\tif( config.showSlideNumber === 'all' ) {\n\t\t\t\tslideNumberDisplay = 'block';\n\t\t\t}\n\t\t\telse if( config.showSlideNumber === 'speaker' && this.Reveal.isSpeakerNotes() ) {\n\t\t\t\tslideNumberDisplay = 'block';\n\t\t\t}\n\t\t}\n\n\t\tthis.element.style.display = slideNumberDisplay;\n\n\t}\n\n\t/**\n\t * Updates the slide number to match the current slide.\n\t */\n\tupdate() {\n\n\t\t// Update slide number if enabled\n\t\tif( this.Reveal.getConfig().slideNumber && this.element ) {\n\t\t\tthis.element.innerHTML = this.getSlideNumber();\n\t\t}\n\n\t}\n\n\t/**\n\t * Returns the HTML string corresponding to the current slide\n\t * number, including formatting.\n\t */\n\tgetSlideNumber( slide = this.Reveal.getCurrentSlide() ) {\n\n\t\tlet config = this.Reveal.getConfig();\n\t\tlet value;\n\t\tlet format = 'h.v';\n\n\t\tif ( typeof config.slideNumber === 'function' ) {\n\t\t\tvalue = config.slideNumber( slide );\n\t\t} else {\n\t\t\t// Check if a custom number format is available\n\t\t\tif( typeof config.slideNumber === 'string' ) {\n\t\t\t\tformat = config.slideNumber;\n\t\t\t}\n\n\t\t\t// If there are ONLY vertical slides in this deck, always use\n\t\t\t// a flattened slide number\n\t\t\tif( !/c/.test( format ) && this.Reveal.getHorizontalSlides().length === 1 ) {\n\t\t\t\tformat = 'c';\n\t\t\t}\n\n\t\t\t// Offset the current slide number by 1 to make it 1-indexed\n\t\t\tlet horizontalOffset = slide && slide.dataset.visibility === 'uncounted' ? 0 : 1;\n\n\t\t\tvalue = [];\n\t\t\tswitch( format ) {\n\t\t\t\tcase 'c':\n\t\t\t\t\tvalue.push( this.Reveal.getSlidePastCount( slide ) + horizontalOffset );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'c/t':\n\t\t\t\t\tvalue.push( this.Reveal.getSlidePastCount( slide ) + horizontalOffset, '/', this.Reveal.getTotalSlides() );\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tlet indices = this.Reveal.getIndices( slide );\n\t\t\t\t\tvalue.push( indices.h + horizontalOffset );\n\t\t\t\t\tlet sep = format === 'h/v' ? '/' : '.';\n\t\t\t\t\tif( this.Reveal.isVerticalSlide( slide ) ) value.push( sep, indices.v + 1 );\n\t\t\t}\n\t\t}\n\n\t\tlet url = '#' + this.Reveal.location.getHash( slide );\n\t\treturn this.formatNumber( value[0], value[1], value[2], url );\n\n\t}\n\n\t/**\n\t * Applies HTML formatting to a slide number before it's\n\t * written to the DOM.\n\t *\n\t * @param {number} a Current slide\n\t * @param {string} delimiter Character to separate slide numbers\n\t * @param {(number|*)} b Total slides\n\t * @param {HTMLElement} [url='#'+locationHash()] The url to link to\n\t * @return {string} HTML string fragment\n\t */\n\tformatNumber( a, delimiter, b, url = '#' + this.Reveal.location.getHash() ) {\n\n\t\tif( typeof b === 'number' && !isNaN( b ) ) {\n\t\t\treturn `\n\t\t\t\t\t${a}\n\t\t\t\t\t${delimiter}\n\t\t\t\t\t${b}\n\t\t\t\t\t`;\n\t\t}\n\t\telse {\n\t\t\treturn `\n\t\t\t\t\t${a}\n\t\t\t\t\t`;\n\t\t}\n\n\t}\n\n\tdestroy() {\n\n\t\tthis.element.remove();\n\n\t}\n\n}","/**\n * Converts various color input formats to an {r:0,g:0,b:0} object.\n *\n * @param {string} color The string representation of a color\n * @example\n * colorToRgb('#000');\n * @example\n * colorToRgb('#000000');\n * @example\n * colorToRgb('rgb(0,0,0)');\n * @example\n * colorToRgb('rgba(0,0,0)');\n *\n * @return {{r: number, g: number, b: number, [a]: number}|null}\n */\nexport const colorToRgb = ( color ) => {\n\n\tlet hex3 = color.match( /^#([0-9a-f]{3})$/i );\n\tif( hex3 && hex3[1] ) {\n\t\thex3 = hex3[1];\n\t\treturn {\n\t\t\tr: parseInt( hex3.charAt( 0 ), 16 ) * 0x11,\n\t\t\tg: parseInt( hex3.charAt( 1 ), 16 ) * 0x11,\n\t\t\tb: parseInt( hex3.charAt( 2 ), 16 ) * 0x11\n\t\t};\n\t}\n\n\tlet hex6 = color.match( /^#([0-9a-f]{6})$/i );\n\tif( hex6 && hex6[1] ) {\n\t\thex6 = hex6[1];\n\t\treturn {\n\t\t\tr: parseInt( hex6.slice( 0, 2 ), 16 ),\n\t\t\tg: parseInt( hex6.slice( 2, 4 ), 16 ),\n\t\t\tb: parseInt( hex6.slice( 4, 6 ), 16 )\n\t\t};\n\t}\n\n\tlet rgb = color.match( /^rgb\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)$/i );\n\tif( rgb ) {\n\t\treturn {\n\t\t\tr: parseInt( rgb[1], 10 ),\n\t\t\tg: parseInt( rgb[2], 10 ),\n\t\t\tb: parseInt( rgb[3], 10 )\n\t\t};\n\t}\n\n\tlet rgba = color.match( /^rgba\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\,\\s*([\\d]+|[\\d]*.[\\d]+)\\s*\\)$/i );\n\tif( rgba ) {\n\t\treturn {\n\t\t\tr: parseInt( rgba[1], 10 ),\n\t\t\tg: parseInt( rgba[2], 10 ),\n\t\t\tb: parseInt( rgba[3], 10 ),\n\t\t\ta: parseFloat( rgba[4] )\n\t\t};\n\t}\n\n\treturn null;\n\n}\n\n/**\n * Calculates brightness on a scale of 0-255.\n *\n * @param {string} color See colorToRgb for supported formats.\n * @see {@link colorToRgb}\n */\nexport const colorBrightness = ( color ) => {\n\n\tif( typeof color === 'string' ) color = colorToRgb( color );\n\n\tif( color ) {\n\t\treturn ( color.r * 299 + color.g * 587 + color.b * 114 ) / 1000;\n\t}\n\n\treturn null;\n\n}","import { queryAll } from '../utils/util.js'\nimport { colorToRgb, colorBrightness } from '../utils/color.js'\n\n/**\n * Creates and updates slide backgrounds.\n */\nexport default class Backgrounds {\n\n\tconstructor( Reveal ) {\n\n\t\tthis.Reveal = Reveal;\n\n\t}\n\n\trender() {\n\n\t\tthis.element = document.createElement( 'div' );\n\t\tthis.element.className = 'backgrounds';\n\t\tthis.Reveal.getRevealElement().appendChild( this.element );\n\n\t}\n\n\t/**\n\t * Creates the slide background elements and appends them\n\t * to the background container. One element is created per\n\t * slide no matter if the given slide has visible background.\n\t */\n\tcreate() {\n\n\t\t// Clear prior backgrounds\n\t\tthis.element.innerHTML = '';\n\t\tthis.element.classList.add( 'no-transition' );\n\n\t\t// Iterate over all horizontal slides\n\t\tthis.Reveal.getHorizontalSlides().forEach( slideh => {\n\n\t\t\tlet backgroundStack = this.createBackground( slideh, this.element );\n\n\t\t\t// Iterate over all vertical slides\n\t\t\tqueryAll( slideh, 'section' ).forEach( slidev => {\n\n\t\t\t\tthis.createBackground( slidev, backgroundStack );\n\n\t\t\t\tbackgroundStack.classList.add( 'stack' );\n\n\t\t\t} );\n\n\t\t} );\n\n\t\t// Add parallax background if specified\n\t\tif( this.Reveal.getConfig().parallaxBackgroundImage ) {\n\n\t\t\tthis.element.style.backgroundImage = 'url(\"' + this.Reveal.getConfig().parallaxBackgroundImage + '\")';\n\t\t\tthis.element.style.backgroundSize = this.Reveal.getConfig().parallaxBackgroundSize;\n\t\t\tthis.element.style.backgroundRepeat = this.Reveal.getConfig().parallaxBackgroundRepeat;\n\t\t\tthis.element.style.backgroundPosition = this.Reveal.getConfig().parallaxBackgroundPosition;\n\n\t\t\t// Make sure the below properties are set on the element - these properties are\n\t\t\t// needed for proper transitions to be set on the element via CSS. To remove\n\t\t\t// annoying background slide-in effect when the presentation starts, apply\n\t\t\t// these properties after short time delay\n\t\t\tsetTimeout( () => {\n\t\t\t\tthis.Reveal.getRevealElement().classList.add( 'has-parallax-background' );\n\t\t\t}, 1 );\n\n\t\t}\n\t\telse {\n\n\t\t\tthis.element.style.backgroundImage = '';\n\t\t\tthis.Reveal.getRevealElement().classList.remove( 'has-parallax-background' );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Creates a background for the given slide.\n\t *\n\t * @param {HTMLElement} slide\n\t * @param {HTMLElement} container The element that the background\n\t * should be appended to\n\t * @return {HTMLElement} New background div\n\t */\n\tcreateBackground( slide, container ) {\n\n\t\t// Main slide background element\n\t\tlet element = document.createElement( 'div' );\n\t\telement.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' );\n\n\t\t// Inner background element that wraps images/videos/iframes\n\t\tlet contentElement = document.createElement( 'div' );\n\t\tcontentElement.className = 'slide-background-content';\n\n\t\telement.appendChild( contentElement );\n\t\tcontainer.appendChild( element );\n\n\t\tslide.slideBackgroundElement = element;\n\t\tslide.slideBackgroundContentElement = contentElement;\n\n\t\t// Syncs the background to reflect all current background settings\n\t\tthis.sync( slide );\n\n\t\treturn element;\n\n\t}\n\n\t/**\n\t * Renders all of the visual properties of a slide background\n\t * based on the various background attributes.\n\t *\n\t * @param {HTMLElement} slide\n\t */\n\tsync( slide ) {\n\n\t\tconst element = slide.slideBackgroundElement,\n\t\t\tcontentElement = slide.slideBackgroundContentElement;\n\n\t\tconst data = {\n\t\t\tbackground: slide.getAttribute( 'data-background' ),\n\t\t\tbackgroundSize: slide.getAttribute( 'data-background-size' ),\n\t\t\tbackgroundImage: slide.getAttribute( 'data-background-image' ),\n\t\t\tbackgroundVideo: slide.getAttribute( 'data-background-video' ),\n\t\t\tbackgroundIframe: slide.getAttribute( 'data-background-iframe' ),\n\t\t\tbackgroundColor: slide.getAttribute( 'data-background-color' ),\n\t\t\tbackgroundGradient: slide.getAttribute( 'data-background-gradient' ),\n\t\t\tbackgroundRepeat: slide.getAttribute( 'data-background-repeat' ),\n\t\t\tbackgroundPosition: slide.getAttribute( 'data-background-position' ),\n\t\t\tbackgroundTransition: slide.getAttribute( 'data-background-transition' ),\n\t\t\tbackgroundOpacity: slide.getAttribute( 'data-background-opacity' ),\n\t\t};\n\n\t\tconst dataPreload = slide.hasAttribute( 'data-preload' );\n\n\t\t// Reset the prior background state in case this is not the\n\t\t// initial sync\n\t\tslide.classList.remove( 'has-dark-background' );\n\t\tslide.classList.remove( 'has-light-background' );\n\n\t\telement.removeAttribute( 'data-loaded' );\n\t\telement.removeAttribute( 'data-background-hash' );\n\t\telement.removeAttribute( 'data-background-size' );\n\t\telement.removeAttribute( 'data-background-transition' );\n\t\telement.style.backgroundColor = '';\n\n\t\tcontentElement.style.backgroundSize = '';\n\t\tcontentElement.style.backgroundRepeat = '';\n\t\tcontentElement.style.backgroundPosition = '';\n\t\tcontentElement.style.backgroundImage = '';\n\t\tcontentElement.style.opacity = '';\n\t\tcontentElement.innerHTML = '';\n\n\t\tif( data.background ) {\n\t\t\t// Auto-wrap image urls in url(...)\n\t\t\tif( /^(http|file|\\/\\/)/gi.test( data.background ) || /\\.(svg|png|jpg|jpeg|gif|bmp|webp)([?#\\s]|$)/gi.test( data.background ) ) {\n\t\t\t\tslide.setAttribute( 'data-background-image', data.background );\n\t\t\t}\n\t\t\telse {\n\t\t\t\telement.style.background = data.background;\n\t\t\t}\n\t\t}\n\n\t\t// Create a hash for this combination of background settings.\n\t\t// This is used to determine when two slide backgrounds are\n\t\t// the same.\n\t\tif( data.background || data.backgroundColor || data.backgroundGradient || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) {\n\t\t\telement.setAttribute( 'data-background-hash', data.background +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdata.backgroundSize +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdata.backgroundImage +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdata.backgroundVideo +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdata.backgroundIframe +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdata.backgroundColor +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdata.backgroundGradient +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdata.backgroundRepeat +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdata.backgroundPosition +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdata.backgroundTransition +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdata.backgroundOpacity );\n\t\t}\n\n\t\t// Additional and optional background properties\n\t\tif( data.backgroundSize ) element.setAttribute( 'data-background-size', data.backgroundSize );\n\t\tif( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor;\n\t\tif( data.backgroundGradient ) element.style.backgroundImage = data.backgroundGradient;\n\t\tif( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition );\n\n\t\tif( dataPreload ) element.setAttribute( 'data-preload', '' );\n\n\t\t// Background image options are set on the content wrapper\n\t\tif( data.backgroundSize ) contentElement.style.backgroundSize = data.backgroundSize;\n\t\tif( data.backgroundRepeat ) contentElement.style.backgroundRepeat = data.backgroundRepeat;\n\t\tif( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition;\n\t\tif( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity;\n\n\t\t// If this slide has a background color, we add a class that\n\t\t// signals if it is light or dark. If the slide has no background\n\t\t// color, no class will be added\n\t\tlet contrastColor = data.backgroundColor;\n\n\t\t// If no bg color was found, or it cannot be converted by colorToRgb, check the computed background\n\t\tif( !contrastColor || !colorToRgb( contrastColor ) ) {\n\t\t\tlet computedBackgroundStyle = window.getComputedStyle( element );\n\t\t\tif( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) {\n\t\t\t\tcontrastColor = computedBackgroundStyle.backgroundColor;\n\t\t\t}\n\t\t}\n\n\t\tif( contrastColor ) {\n\t\t\tconst rgb = colorToRgb( contrastColor );\n\n\t\t\t// Ignore fully transparent backgrounds. Some browsers return\n\t\t\t// rgba(0,0,0,0) when reading the computed background color of\n\t\t\t// an element with no background\n\t\t\tif( rgb && rgb.a !== 0 ) {\n\t\t\t\tif( colorBrightness( contrastColor ) < 128 ) {\n\t\t\t\t\tslide.classList.add( 'has-dark-background' );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tslide.classList.add( 'has-light-background' );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Updates the background elements to reflect the current\n\t * slide.\n\t *\n\t * @param {boolean} includeAll If true, the backgrounds of\n\t * all vertical slides (not just the present) will be updated.\n\t */\n\tupdate( includeAll = false ) {\n\n\t\tlet currentSlide = this.Reveal.getCurrentSlide();\n\t\tlet indices = this.Reveal.getIndices();\n\n\t\tlet currentBackground = null;\n\n\t\t// Reverse past/future classes when in RTL mode\n\t\tlet horizontalPast = this.Reveal.getConfig().rtl ? 'future' : 'past',\n\t\t\thorizontalFuture = this.Reveal.getConfig().rtl ? 'past' : 'future';\n\n\t\t// Update the classes of all backgrounds to match the\n\t\t// states of their slides (past/present/future)\n\t\tArray.from( this.element.childNodes ).forEach( ( backgroundh, h ) => {\n\n\t\t\tbackgroundh.classList.remove( 'past', 'present', 'future' );\n\n\t\t\tif( h < indices.h ) {\n\t\t\t\tbackgroundh.classList.add( horizontalPast );\n\t\t\t}\n\t\t\telse if ( h > indices.h ) {\n\t\t\t\tbackgroundh.classList.add( horizontalFuture );\n\t\t\t}\n\t\t\telse {\n\t\t\t\tbackgroundh.classList.add( 'present' );\n\n\t\t\t\t// Store a reference to the current background element\n\t\t\t\tcurrentBackground = backgroundh;\n\t\t\t}\n\n\t\t\tif( includeAll || h === indices.h ) {\n\t\t\t\tqueryAll( backgroundh, '.slide-background' ).forEach( ( backgroundv, v ) => {\n\n\t\t\t\t\tbackgroundv.classList.remove( 'past', 'present', 'future' );\n\n\t\t\t\t\tif( v < indices.v ) {\n\t\t\t\t\t\tbackgroundv.classList.add( 'past' );\n\t\t\t\t\t}\n\t\t\t\t\telse if ( v > indices.v ) {\n\t\t\t\t\t\tbackgroundv.classList.add( 'future' );\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tbackgroundv.classList.add( 'present' );\n\n\t\t\t\t\t\t// Only if this is the present horizontal and vertical slide\n\t\t\t\t\t\tif( h === indices.h ) currentBackground = backgroundv;\n\t\t\t\t\t}\n\n\t\t\t\t} );\n\t\t\t}\n\n\t\t} );\n\n\t\t// Stop content inside of previous backgrounds\n\t\tif( this.previousBackground ) {\n\n\t\t\tthis.Reveal.slideContent.stopEmbeddedContent( this.previousBackground, { unloadIframes: !this.Reveal.slideContent.shouldPreload( this.previousBackground ) } );\n\n\t\t}\n\n\t\t// Start content in the current background\n\t\tif( currentBackground ) {\n\n\t\t\tthis.Reveal.slideContent.startEmbeddedContent( currentBackground );\n\n\t\t\tlet currentBackgroundContent = currentBackground.querySelector( '.slide-background-content' );\n\t\t\tif( currentBackgroundContent ) {\n\n\t\t\t\tlet backgroundImageURL = currentBackgroundContent.style.backgroundImage || '';\n\n\t\t\t\t// Restart GIFs (doesn't work in Firefox)\n\t\t\t\tif( /\\.gif/i.test( backgroundImageURL ) ) {\n\t\t\t\t\tcurrentBackgroundContent.style.backgroundImage = '';\n\t\t\t\t\twindow.getComputedStyle( currentBackgroundContent ).opacity;\n\t\t\t\t\tcurrentBackgroundContent.style.backgroundImage = backgroundImageURL;\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// Don't transition between identical backgrounds. This\n\t\t\t// prevents unwanted flicker.\n\t\t\tlet previousBackgroundHash = this.previousBackground ? this.previousBackground.getAttribute( 'data-background-hash' ) : null;\n\t\t\tlet currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' );\n\t\t\tif( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== this.previousBackground ) {\n\t\t\t\tthis.element.classList.add( 'no-transition' );\n\t\t\t}\n\n\t\t\tthis.previousBackground = currentBackground;\n\n\t\t}\n\n\t\t// If there's a background brightness flag for this slide,\n\t\t// bubble it to the .reveal container\n\t\tif( currentSlide ) {\n\t\t\t[ 'has-light-background', 'has-dark-background' ].forEach( classToBubble => {\n\t\t\t\tif( currentSlide.classList.contains( classToBubble ) ) {\n\t\t\t\t\tthis.Reveal.getRevealElement().classList.add( classToBubble );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tthis.Reveal.getRevealElement().classList.remove( classToBubble );\n\t\t\t\t}\n\t\t\t}, this );\n\t\t}\n\n\t\t// Allow the first background to apply without transition\n\t\tsetTimeout( () => {\n\t\t\tthis.element.classList.remove( 'no-transition' );\n\t\t}, 1 );\n\n\t}\n\n\t/**\n\t * Updates the position of the parallax background based\n\t * on the current slide index.\n\t */\n\tupdateParallax() {\n\n\t\tlet indices = this.Reveal.getIndices();\n\n\t\tif( this.Reveal.getConfig().parallaxBackgroundImage ) {\n\n\t\t\tlet horizontalSlides = this.Reveal.getHorizontalSlides(),\n\t\t\t\tverticalSlides = this.Reveal.getVerticalSlides();\n\n\t\t\tlet backgroundSize = this.element.style.backgroundSize.split( ' ' ),\n\t\t\t\tbackgroundWidth, backgroundHeight;\n\n\t\t\tif( backgroundSize.length === 1 ) {\n\t\t\t\tbackgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 );\n\t\t\t}\n\t\t\telse {\n\t\t\t\tbackgroundWidth = parseInt( backgroundSize[0], 10 );\n\t\t\t\tbackgroundHeight = parseInt( backgroundSize[1], 10 );\n\t\t\t}\n\n\t\t\tlet slideWidth = this.element.offsetWidth,\n\t\t\t\thorizontalSlideCount = horizontalSlides.length,\n\t\t\t\thorizontalOffsetMultiplier,\n\t\t\t\thorizontalOffset;\n\n\t\t\tif( typeof this.Reveal.getConfig().parallaxBackgroundHorizontal === 'number' ) {\n\t\t\t\thorizontalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundHorizontal;\n\t\t\t}\n\t\t\telse {\n\t\t\t\thorizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0;\n\t\t\t}\n\n\t\t\thorizontalOffset = horizontalOffsetMultiplier * indices.h * -1;\n\n\t\t\tlet slideHeight = this.element.offsetHeight,\n\t\t\t\tverticalSlideCount = verticalSlides.length,\n\t\t\t\tverticalOffsetMultiplier,\n\t\t\t\tverticalOffset;\n\n\t\t\tif( typeof this.Reveal.getConfig().parallaxBackgroundVertical === 'number' ) {\n\t\t\t\tverticalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundVertical;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tverticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );\n\t\t\t}\n\n\t\t\tverticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indices.v : 0;\n\n\t\t\tthis.element.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';\n\n\t\t}\n\n\t}\n\n\tdestroy() {\n\n\t\tthis.element.remove();\n\n\t}\n\n}\n","\nexport const SLIDES_SELECTOR = '.slides section';\nexport const HORIZONTAL_SLIDES_SELECTOR = '.slides>section';\nexport const VERTICAL_SLIDES_SELECTOR = '.slides>section.present>section';\n\n// Methods that may not be invoked via the postMessage API\nexport const POST_MESSAGE_METHOD_BLACKLIST = /registerPlugin|registerKeyboardShortcut|addKeyBinding|addEventListener|showPreview/;\n\n// Regex for retrieving the fragment style from a class attribute\nexport const FRAGMENT_STYLE_REGEX = /fade-(down|up|right|left|out|in-then-out|in-then-semi-out)|semi-fade-out|current-visible|shrink|grow/;","import { queryAll, extend, createStyleSheet, matches, closest } from '../utils/util.js'\nimport { FRAGMENT_STYLE_REGEX } from '../utils/constants.js'\n\n// Counter used to generate unique IDs for auto-animated elements\nlet autoAnimateCounter = 0;\n\n/**\n * Automatically animates matching elements across\n * slides with the [data-auto-animate] attribute.\n */\nexport default class AutoAnimate {\n\n\tconstructor( Reveal ) {\n\n\t\tthis.Reveal = Reveal;\n\n\t}\n\n\t/**\n\t * Runs an auto-animation between the given slides.\n\t *\n\t * @param {HTMLElement} fromSlide\n\t * @param {HTMLElement} toSlide\n\t */\n\trun( fromSlide, toSlide ) {\n\n\t\t// Clean up after prior animations\n\t\tthis.reset();\n\n\t\tlet allSlides = this.Reveal.getSlides();\n\t\tlet toSlideIndex = allSlides.indexOf( toSlide );\n\t\tlet fromSlideIndex = allSlides.indexOf( fromSlide );\n\n\t\t// Ensure that both slides are auto-animate targets with the same data-auto-animate-id value\n\t\t// (including null if absent on both) and that data-auto-animate-restart isn't set on the\n\t\t// physically latter slide (independent of slide direction)\n\t\tif( fromSlide.hasAttribute( 'data-auto-animate' ) && toSlide.hasAttribute( 'data-auto-animate' )\n\t\t\t\t&& fromSlide.getAttribute( 'data-auto-animate-id' ) === toSlide.getAttribute( 'data-auto-animate-id' ) \n\t\t\t\t&& !( toSlideIndex > fromSlideIndex ? toSlide : fromSlide ).hasAttribute( 'data-auto-animate-restart' ) ) {\n\n\t\t\t// Create a new auto-animate sheet\n\t\t\tthis.autoAnimateStyleSheet = this.autoAnimateStyleSheet || createStyleSheet();\n\n\t\t\tlet animationOptions = this.getAutoAnimateOptions( toSlide );\n\n\t\t\t// Set our starting state\n\t\t\tfromSlide.dataset.autoAnimate = 'pending';\n\t\t\ttoSlide.dataset.autoAnimate = 'pending';\n\n\t\t\t// Flag the navigation direction, needed for fragment buildup\n\t\t\tanimationOptions.slideDirection = toSlideIndex > fromSlideIndex ? 'forward' : 'backward';\n\n\t\t\t// If the from-slide is hidden because it has moved outside\n\t\t\t// the view distance, we need to temporarily show it while\n\t\t\t// measuring\n\t\t\tlet fromSlideIsHidden = fromSlide.style.display === 'none';\n\t\t\tif( fromSlideIsHidden ) fromSlide.style.display = this.Reveal.getConfig().display;\n\n\t\t\t// Inject our auto-animate styles for this transition\n\t\t\tlet css = this.getAutoAnimatableElements( fromSlide, toSlide ).map( elements => {\n\t\t\t\treturn this.autoAnimateElements( elements.from, elements.to, elements.options || {}, animationOptions, autoAnimateCounter++ );\n\t\t\t} );\n\n\t\t\tif( fromSlideIsHidden ) fromSlide.style.display = 'none';\n\n\t\t\t// Animate unmatched elements, if enabled\n\t\t\tif( toSlide.dataset.autoAnimateUnmatched !== 'false' && this.Reveal.getConfig().autoAnimateUnmatched === true ) {\n\n\t\t\t\t// Our default timings for unmatched elements\n\t\t\t\tlet defaultUnmatchedDuration = animationOptions.duration * 0.8,\n\t\t\t\t\tdefaultUnmatchedDelay = animationOptions.duration * 0.2;\n\n\t\t\t\tthis.getUnmatchedAutoAnimateElements( toSlide ).forEach( unmatchedElement => {\n\n\t\t\t\t\tlet unmatchedOptions = this.getAutoAnimateOptions( unmatchedElement, animationOptions );\n\t\t\t\t\tlet id = 'unmatched';\n\n\t\t\t\t\t// If there is a duration or delay set specifically for this\n\t\t\t\t\t// element our unmatched elements should adhere to those\n\t\t\t\t\tif( unmatchedOptions.duration !== animationOptions.duration || unmatchedOptions.delay !== animationOptions.delay ) {\n\t\t\t\t\t\tid = 'unmatched-' + autoAnimateCounter++;\n\t\t\t\t\t\tcss.push( `[data-auto-animate=\"running\"] [data-auto-animate-target=\"${id}\"] { transition: opacity ${unmatchedOptions.duration}s ease ${unmatchedOptions.delay}s; }` );\n\t\t\t\t\t}\n\n\t\t\t\t\tunmatchedElement.dataset.autoAnimateTarget = id;\n\n\t\t\t\t}, this );\n\n\t\t\t\t// Our default transition for unmatched elements\n\t\t\t\tcss.push( `[data-auto-animate=\"running\"] [data-auto-animate-target=\"unmatched\"] { transition: opacity ${defaultUnmatchedDuration}s ease ${defaultUnmatchedDelay}s; }` );\n\n\t\t\t}\n\n\t\t\t// Setting the whole chunk of CSS at once is the most\n\t\t\t// efficient way to do this. Using sheet.insertRule\n\t\t\t// is multiple factors slower.\n\t\t\tthis.autoAnimateStyleSheet.innerHTML = css.join( '' );\n\n\t\t\t// Start the animation next cycle\n\t\t\trequestAnimationFrame( () => {\n\t\t\t\tif( this.autoAnimateStyleSheet ) {\n\t\t\t\t\t// This forces our newly injected styles to be applied in Firefox\n\t\t\t\t\tgetComputedStyle( this.autoAnimateStyleSheet ).fontWeight;\n\n\t\t\t\t\ttoSlide.dataset.autoAnimate = 'running';\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\tthis.Reveal.dispatchEvent({\n\t\t\t\ttype: 'autoanimate',\n\t\t\t\tdata: {\n\t\t\t\t\tfromSlide,\n\t\t\t\t\ttoSlide,\n\t\t\t\t\tsheet: this.autoAnimateStyleSheet\n\t\t\t\t}\n\t\t\t});\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Rolls back all changes that we've made to the DOM so\n\t * that as part of animating.\n\t */\n\treset() {\n\n\t\t// Reset slides\n\t\tqueryAll( this.Reveal.getRevealElement(), '[data-auto-animate]:not([data-auto-animate=\"\"])' ).forEach( element => {\n\t\t\telement.dataset.autoAnimate = '';\n\t\t} );\n\n\t\t// Reset elements\n\t\tqueryAll( this.Reveal.getRevealElement(), '[data-auto-animate-target]' ).forEach( element => {\n\t\t\tdelete element.dataset.autoAnimateTarget;\n\t\t} );\n\n\t\t// Remove the animation sheet\n\t\tif( this.autoAnimateStyleSheet && this.autoAnimateStyleSheet.parentNode ) {\n\t\t\tthis.autoAnimateStyleSheet.parentNode.removeChild( this.autoAnimateStyleSheet );\n\t\t\tthis.autoAnimateStyleSheet = null;\n\t\t}\n\n\t}\n\n\t/**\n\t * Creates a FLIP animation where the `to` element starts out\n\t * in the `from` element position and animates to its original\n\t * state.\n\t *\n\t * @param {HTMLElement} from\n\t * @param {HTMLElement} to\n\t * @param {Object} elementOptions Options for this element pair\n\t * @param {Object} animationOptions Options set at the slide level\n\t * @param {String} id Unique ID that we can use to identify this\n\t * auto-animate element in the DOM\n\t */\n\tautoAnimateElements( from, to, elementOptions, animationOptions, id ) {\n\n\t\t// 'from' elements are given a data-auto-animate-target with no value,\n\t\t// 'to' elements are are given a data-auto-animate-target with an ID\n\t\tfrom.dataset.autoAnimateTarget = '';\n\t\tto.dataset.autoAnimateTarget = id;\n\n\t\t// Each element may override any of the auto-animate options\n\t\t// like transition easing, duration and delay via data-attributes\n\t\tlet options = this.getAutoAnimateOptions( to, animationOptions );\n\n\t\t// If we're using a custom element matcher the element options\n\t\t// may contain additional transition overrides\n\t\tif( typeof elementOptions.delay !== 'undefined' ) options.delay = elementOptions.delay;\n\t\tif( typeof elementOptions.duration !== 'undefined' ) options.duration = elementOptions.duration;\n\t\tif( typeof elementOptions.easing !== 'undefined' ) options.easing = elementOptions.easing;\n\n\t\tlet fromProps = this.getAutoAnimatableProperties( 'from', from, elementOptions ),\n\t\t\ttoProps = this.getAutoAnimatableProperties( 'to', to, elementOptions );\n\n\t\t// Maintain fragment visibility for matching elements when\n\t\t// we're navigating forwards, this way the viewer won't need\n\t\t// to step through the same fragments twice\n\t\tif( to.classList.contains( 'fragment' ) ) {\n\n\t\t\t// Don't auto-animate the opacity of fragments to avoid\n\t\t\t// conflicts with fragment animations\n\t\t\tdelete toProps.styles['opacity'];\n\n\t\t\tif( from.classList.contains( 'fragment' ) ) {\n\n\t\t\t\tlet fromFragmentStyle = ( from.className.match( FRAGMENT_STYLE_REGEX ) || [''] )[0];\n\t\t\t\tlet toFragmentStyle = ( to.className.match( FRAGMENT_STYLE_REGEX ) || [''] )[0];\n\n\t\t\t\t// Only skip the fragment if the fragment animation style\n\t\t\t\t// remains unchanged\n\t\t\t\tif( fromFragmentStyle === toFragmentStyle && animationOptions.slideDirection === 'forward' ) {\n\t\t\t\t\tto.classList.add( 'visible', 'disabled' );\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\t// If translation and/or scaling are enabled, css transform\n\t\t// the 'to' element so that it matches the position and size\n\t\t// of the 'from' element\n\t\tif( elementOptions.translate !== false || elementOptions.scale !== false ) {\n\n\t\t\tlet presentationScale = this.Reveal.getScale();\n\n\t\t\tlet delta = {\n\t\t\t\tx: ( fromProps.x - toProps.x ) / presentationScale,\n\t\t\t\ty: ( fromProps.y - toProps.y ) / presentationScale,\n\t\t\t\tscaleX: fromProps.width / toProps.width,\n\t\t\t\tscaleY: fromProps.height / toProps.height\n\t\t\t};\n\n\t\t\t// Limit decimal points to avoid 0.0001px blur and stutter\n\t\t\tdelta.x = Math.round( delta.x * 1000 ) / 1000;\n\t\t\tdelta.y = Math.round( delta.y * 1000 ) / 1000;\n\t\t\tdelta.scaleX = Math.round( delta.scaleX * 1000 ) / 1000;\n\t\t\tdelta.scaleX = Math.round( delta.scaleX * 1000 ) / 1000;\n\n\t\t\tlet translate = elementOptions.translate !== false && ( delta.x !== 0 || delta.y !== 0 ),\n\t\t\t\tscale = elementOptions.scale !== false && ( delta.scaleX !== 0 || delta.scaleY !== 0 );\n\n\t\t\t// No need to transform if nothing's changed\n\t\t\tif( translate || scale ) {\n\n\t\t\t\tlet transform = [];\n\n\t\t\t\tif( translate ) transform.push( `translate(${delta.x}px, ${delta.y}px)` );\n\t\t\t\tif( scale ) transform.push( `scale(${delta.scaleX}, ${delta.scaleY})` );\n\n\t\t\t\tfromProps.styles['transform'] = transform.join( ' ' );\n\t\t\t\tfromProps.styles['transform-origin'] = 'top left';\n\n\t\t\t\ttoProps.styles['transform'] = 'none';\n\n\t\t\t}\n\n\t\t}\n\n\t\t// Delete all unchanged 'to' styles\n\t\tfor( let propertyName in toProps.styles ) {\n\t\t\tconst toValue = toProps.styles[propertyName];\n\t\t\tconst fromValue = fromProps.styles[propertyName];\n\n\t\t\tif( toValue === fromValue ) {\n\t\t\t\tdelete toProps.styles[propertyName];\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// If these property values were set via a custom matcher providing\n\t\t\t\t// an explicit 'from' and/or 'to' value, we always inject those values.\n\t\t\t\tif( toValue.explicitValue === true ) {\n\t\t\t\t\ttoProps.styles[propertyName] = toValue.value;\n\t\t\t\t}\n\n\t\t\t\tif( fromValue.explicitValue === true ) {\n\t\t\t\t\tfromProps.styles[propertyName] = fromValue.value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlet css = '';\n\n\t\tlet toStyleProperties = Object.keys( toProps.styles );\n\n\t\t// Only create animate this element IF at least one style\n\t\t// property has changed\n\t\tif( toStyleProperties.length > 0 ) {\n\n\t\t\t// Instantly move to the 'from' state\n\t\t\tfromProps.styles['transition'] = 'none';\n\n\t\t\t// Animate towards the 'to' state\n\t\t\ttoProps.styles['transition'] = `all ${options.duration}s ${options.easing} ${options.delay}s`;\n\t\t\ttoProps.styles['transition-property'] = toStyleProperties.join( ', ' );\n\t\t\ttoProps.styles['will-change'] = toStyleProperties.join( ', ' );\n\n\t\t\t// Build up our custom CSS. We need to override inline styles\n\t\t\t// so we need to make our styles vErY IMPORTANT!1!!\n\t\t\tlet fromCSS = Object.keys( fromProps.styles ).map( propertyName => {\n\t\t\t\treturn propertyName + ': ' + fromProps.styles[propertyName] + ' !important;';\n\t\t\t} ).join( '' );\n\n\t\t\tlet toCSS = Object.keys( toProps.styles ).map( propertyName => {\n\t\t\t\treturn propertyName + ': ' + toProps.styles[propertyName] + ' !important;';\n\t\t\t} ).join( '' );\n\n\t\t\tcss = \t'[data-auto-animate-target=\"'+ id +'\"] {'+ fromCSS +'}' +\n\t\t\t\t\t'[data-auto-animate=\"running\"] [data-auto-animate-target=\"'+ id +'\"] {'+ toCSS +'}';\n\n\t\t}\n\n\t\treturn css;\n\n\t}\n\n\t/**\n\t * Returns the auto-animate options for the given element.\n\t *\n\t * @param {HTMLElement} element Element to pick up options\n\t * from, either a slide or an animation target\n\t * @param {Object} [inheritedOptions] Optional set of existing\n\t * options\n\t */\n\tgetAutoAnimateOptions( element, inheritedOptions ) {\n\n\t\tlet options = {\n\t\t\teasing: this.Reveal.getConfig().autoAnimateEasing,\n\t\t\tduration: this.Reveal.getConfig().autoAnimateDuration,\n\t\t\tdelay: 0\n\t\t};\n\n\t\toptions = extend( options, inheritedOptions );\n\n\t\t// Inherit options from parent elements\n\t\tif( element.parentNode ) {\n\t\t\tlet autoAnimatedParent = closest( element.parentNode, '[data-auto-animate-target]' );\n\t\t\tif( autoAnimatedParent ) {\n\t\t\t\toptions = this.getAutoAnimateOptions( autoAnimatedParent, options );\n\t\t\t}\n\t\t}\n\n\t\tif( element.dataset.autoAnimateEasing ) {\n\t\t\toptions.easing = element.dataset.autoAnimateEasing;\n\t\t}\n\n\t\tif( element.dataset.autoAnimateDuration ) {\n\t\t\toptions.duration = parseFloat( element.dataset.autoAnimateDuration );\n\t\t}\n\n\t\tif( element.dataset.autoAnimateDelay ) {\n\t\t\toptions.delay = parseFloat( element.dataset.autoAnimateDelay );\n\t\t}\n\n\t\treturn options;\n\n\t}\n\n\t/**\n\t * Returns an object containing all of the properties\n\t * that can be auto-animated for the given element and\n\t * their current computed values.\n\t *\n\t * @param {String} direction 'from' or 'to'\n\t */\n\tgetAutoAnimatableProperties( direction, element, elementOptions ) {\n\n\t\tlet config = this.Reveal.getConfig();\n\n\t\tlet properties = { styles: [] };\n\n\t\t// Position and size\n\t\tif( elementOptions.translate !== false || elementOptions.scale !== false ) {\n\t\t\tlet bounds;\n\n\t\t\t// Custom auto-animate may optionally return a custom tailored\n\t\t\t// measurement function\n\t\t\tif( typeof elementOptions.measure === 'function' ) {\n\t\t\t\tbounds = elementOptions.measure( element );\n\t\t\t}\n\t\t\telse {\n\t\t\t\tif( config.center ) {\n\t\t\t\t\t// More precise, but breaks when used in combination\n\t\t\t\t\t// with zoom for scaling the deck ¯\\_(ツ)_/¯\n\t\t\t\t\tbounds = element.getBoundingClientRect();\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tlet scale = this.Reveal.getScale();\n\t\t\t\t\tbounds = {\n\t\t\t\t\t\tx: element.offsetLeft * scale,\n\t\t\t\t\t\ty: element.offsetTop * scale,\n\t\t\t\t\t\twidth: element.offsetWidth * scale,\n\t\t\t\t\t\theight: element.offsetHeight * scale\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tproperties.x = bounds.x;\n\t\t\tproperties.y = bounds.y;\n\t\t\tproperties.width = bounds.width;\n\t\t\tproperties.height = bounds.height;\n\t\t}\n\n\t\tconst computedStyles = getComputedStyle( element );\n\n\t\t// CSS styles\n\t\t( elementOptions.styles || config.autoAnimateStyles ).forEach( style => {\n\t\t\tlet value;\n\n\t\t\t// `style` is either the property name directly, or an object\n\t\t\t// definition of a style property\n\t\t\tif( typeof style === 'string' ) style = { property: style };\n\n\t\t\tif( typeof style.from !== 'undefined' && direction === 'from' ) {\n\t\t\t\tvalue = { value: style.from, explicitValue: true };\n\t\t\t}\n\t\t\telse if( typeof style.to !== 'undefined' && direction === 'to' ) {\n\t\t\t\tvalue = { value: style.to, explicitValue: true };\n\t\t\t}\n\t\t\telse {\n\t\t\t\tvalue = computedStyles[style.property];\n\t\t\t}\n\n\t\t\tif( value !== '' ) {\n\t\t\t\tproperties.styles[style.property] = value;\n\t\t\t}\n\t\t} );\n\n\t\treturn properties;\n\n\t}\n\n\t/**\n\t * Get a list of all element pairs that we can animate\n\t * between the given slides.\n\t *\n\t * @param {HTMLElement} fromSlide\n\t * @param {HTMLElement} toSlide\n\t *\n\t * @return {Array} Each value is an array where [0] is\n\t * the element we're animating from and [1] is the\n\t * element we're animating to\n\t */\n\tgetAutoAnimatableElements( fromSlide, toSlide ) {\n\n\t\tlet matcher = typeof this.Reveal.getConfig().autoAnimateMatcher === 'function' ? this.Reveal.getConfig().autoAnimateMatcher : this.getAutoAnimatePairs;\n\n\t\tlet pairs = matcher.call( this, fromSlide, toSlide );\n\n\t\tlet reserved = [];\n\n\t\t// Remove duplicate pairs\n\t\treturn pairs.filter( ( pair, index ) => {\n\t\t\tif( reserved.indexOf( pair.to ) === -1 ) {\n\t\t\t\treserved.push( pair.to );\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} );\n\n\t}\n\n\t/**\n\t * Identifies matching elements between slides.\n\t *\n\t * You can specify a custom matcher function by using\n\t * the `autoAnimateMatcher` config option.\n\t */\n\tgetAutoAnimatePairs( fromSlide, toSlide ) {\n\n\t\tlet pairs = [];\n\n\t\tconst codeNodes = 'pre';\n\t\tconst textNodes = 'h1, h2, h3, h4, h5, h6, p, li';\n\t\tconst mediaNodes = 'img, video, iframe';\n\n\t\t// Eplicit matches via data-id\n\t\tthis.findAutoAnimateMatches( pairs, fromSlide, toSlide, '[data-id]', node => {\n\t\t\treturn node.nodeName + ':::' + node.getAttribute( 'data-id' );\n\t\t} );\n\n\t\t// Text\n\t\tthis.findAutoAnimateMatches( pairs, fromSlide, toSlide, textNodes, node => {\n\t\t\treturn node.nodeName + ':::' + node.innerText;\n\t\t} );\n\n\t\t// Media\n\t\tthis.findAutoAnimateMatches( pairs, fromSlide, toSlide, mediaNodes, node => {\n\t\t\treturn node.nodeName + ':::' + ( node.getAttribute( 'src' ) || node.getAttribute( 'data-src' ) );\n\t\t} );\n\n\t\t// Code\n\t\tthis.findAutoAnimateMatches( pairs, fromSlide, toSlide, codeNodes, node => {\n\t\t\treturn node.nodeName + ':::' + node.innerText;\n\t\t} );\n\n\t\tpairs.forEach( pair => {\n\n\t\t\t// Disable scale transformations on text nodes, we transition\n\t\t\t// each individual text property instead\n\t\t\tif( matches( pair.from, textNodes ) ) {\n\t\t\t\tpair.options = { scale: false };\n\t\t\t}\n\t\t\t// Animate individual lines of code\n\t\t\telse if( matches( pair.from, codeNodes ) ) {\n\n\t\t\t\t// Transition the code block's width and height instead of scaling\n\t\t\t\t// to prevent its content from being squished\n\t\t\t\tpair.options = { scale: false, styles: [ 'width', 'height' ] };\n\n\t\t\t\t// Lines of code\n\t\t\t\tthis.findAutoAnimateMatches( pairs, pair.from, pair.to, '.hljs .hljs-ln-code', node => {\n\t\t\t\t\treturn node.textContent;\n\t\t\t\t}, {\n\t\t\t\t\tscale: false,\n\t\t\t\t\tstyles: [],\n\t\t\t\t\tmeasure: this.getLocalBoundingBox.bind( this )\n\t\t\t\t} );\n\n\t\t\t\t// Line numbers\n\t\t\t\tthis.findAutoAnimateMatches( pairs, pair.from, pair.to, '.hljs .hljs-ln-line[data-line-number]', node => {\n\t\t\t\t\treturn node.getAttribute( 'data-line-number' );\n\t\t\t\t}, {\n\t\t\t\t\tscale: false,\n\t\t\t\t\tstyles: [ 'width' ],\n\t\t\t\t\tmeasure: this.getLocalBoundingBox.bind( this )\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t}, this );\n\n\t\treturn pairs;\n\n\t}\n\n\t/**\n\t * Helper method which returns a bounding box based on\n\t * the given elements offset coordinates.\n\t *\n\t * @param {HTMLElement} element\n\t * @return {Object} x, y, width, height\n\t */\n\tgetLocalBoundingBox( element ) {\n\n\t\tconst presentationScale = this.Reveal.getScale();\n\n\t\treturn {\n\t\t\tx: Math.round( ( element.offsetLeft * presentationScale ) * 100 ) / 100,\n\t\t\ty: Math.round( ( element.offsetTop * presentationScale ) * 100 ) / 100,\n\t\t\twidth: Math.round( ( element.offsetWidth * presentationScale ) * 100 ) / 100,\n\t\t\theight: Math.round( ( element.offsetHeight * presentationScale ) * 100 ) / 100\n\t\t};\n\n\t}\n\n\t/**\n\t * Finds matching elements between two slides.\n\t *\n\t * @param {Array} pairs \tList of pairs to push matches to\n\t * @param {HTMLElement} fromScope Scope within the from element exists\n\t * @param {HTMLElement} toScope Scope within the to element exists\n\t * @param {String} selector CSS selector of the element to match\n\t * @param {Function} serializer A function that accepts an element and returns\n\t * a stringified ID based on its contents\n\t * @param {Object} animationOptions Optional config options for this pair\n\t */\n\tfindAutoAnimateMatches( pairs, fromScope, toScope, selector, serializer, animationOptions ) {\n\n\t\tlet fromMatches = {};\n\t\tlet toMatches = {};\n\n\t\t[].slice.call( fromScope.querySelectorAll( selector ) ).forEach( ( element, i ) => {\n\t\t\tconst key = serializer( element );\n\t\t\tif( typeof key === 'string' && key.length ) {\n\t\t\t\tfromMatches[key] = fromMatches[key] || [];\n\t\t\t\tfromMatches[key].push( element );\n\t\t\t}\n\t\t} );\n\n\t\t[].slice.call( toScope.querySelectorAll( selector ) ).forEach( ( element, i ) => {\n\t\t\tconst key = serializer( element );\n\t\t\ttoMatches[key] = toMatches[key] || [];\n\t\t\ttoMatches[key].push( element );\n\n\t\t\tlet fromElement;\n\n\t\t\t// Retrieve the 'from' element\n\t\t\tif( fromMatches[key] ) {\n\t\t\t\tconst pimaryIndex = toMatches[key].length - 1;\n\t\t\t\tconst secondaryIndex = fromMatches[key].length - 1;\n\n\t\t\t\t// If there are multiple identical from elements, retrieve\n\t\t\t\t// the one at the same index as our to-element.\n\t\t\t\tif( fromMatches[key][ pimaryIndex ] ) {\n\t\t\t\t\tfromElement = fromMatches[key][ pimaryIndex ];\n\t\t\t\t\tfromMatches[key][ pimaryIndex ] = null;\n\t\t\t\t}\n\t\t\t\t// If there are no matching from-elements at the same index,\n\t\t\t\t// use the last one.\n\t\t\t\telse if( fromMatches[key][ secondaryIndex ] ) {\n\t\t\t\t\tfromElement = fromMatches[key][ secondaryIndex ];\n\t\t\t\t\tfromMatches[key][ secondaryIndex ] = null;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// If we've got a matching pair, push it to the list of pairs\n\t\t\tif( fromElement ) {\n\t\t\t\tpairs.push({\n\t\t\t\t\tfrom: fromElement,\n\t\t\t\t\tto: element,\n\t\t\t\t\toptions: animationOptions\n\t\t\t\t});\n\t\t\t}\n\t\t} );\n\n\t}\n\n\t/**\n\t * Returns a all elements within the given scope that should\n\t * be considered unmatched in an auto-animate transition. If\n\t * fading of unmatched elements is turned on, these elements\n\t * will fade when going between auto-animate slides.\n\t *\n\t * Note that parents of auto-animate targets are NOT considerd\n\t * unmatched since fading them would break the auto-animation.\n\t *\n\t * @param {HTMLElement} rootElement\n\t * @return {Array}\n\t */\n\tgetUnmatchedAutoAnimateElements( rootElement ) {\n\n\t\treturn [].slice.call( rootElement.children ).reduce( ( result, element ) => {\n\n\t\t\tconst containsAnimatedElements = element.querySelector( '[data-auto-animate-target]' );\n\n\t\t\t// The element is unmatched if\n\t\t\t// - It is not an auto-animate target\n\t\t\t// - It does not contain any auto-animate targets\n\t\t\tif( !element.hasAttribute( 'data-auto-animate-target' ) && !containsAnimatedElements ) {\n\t\t\t\tresult.push( element );\n\t\t\t}\n\n\t\t\tif( element.querySelector( '[data-auto-animate-target]' ) ) {\n\t\t\t\tresult = result.concat( this.getUnmatchedAutoAnimateElements( element ) );\n\t\t\t}\n\n\t\t\treturn result;\n\n\t\t}, [] );\n\n\t}\n\n}\n","import { extend, queryAll } from '../utils/util.js'\n\n/**\n * Handles sorting and navigation of slide fragments.\n * Fragments are elements within a slide that are\n * revealed/animated incrementally.\n */\nexport default class Fragments {\n\n\tconstructor( Reveal ) {\n\n\t\tthis.Reveal = Reveal;\n\n\t}\n\n\t/**\n\t * Called when the reveal.js config is updated.\n\t */\n\tconfigure( config, oldConfig ) {\n\n\t\tif( config.fragments === false ) {\n\t\t\tthis.disable();\n\t\t}\n\t\telse if( oldConfig.fragments === false ) {\n\t\t\tthis.enable();\n\t\t}\n\n\t}\n\n\t/**\n\t * If fragments are disabled in the deck, they should all be\n\t * visible rather than stepped through.\n\t */\n\tdisable() {\n\n\t\tqueryAll( this.Reveal.getSlidesElement(), '.fragment' ).forEach( element => {\n\t\t\telement.classList.add( 'visible' );\n\t\t\telement.classList.remove( 'current-fragment' );\n\t\t} );\n\n\t}\n\n\t/**\n\t * Reverse of #disable(). Only called if fragments have\n\t * previously been disabled.\n\t */\n\tenable() {\n\n\t\tqueryAll( this.Reveal.getSlidesElement(), '.fragment' ).forEach( element => {\n\t\t\telement.classList.remove( 'visible' );\n\t\t\telement.classList.remove( 'current-fragment' );\n\t\t} );\n\n\t}\n\n\t/**\n\t * Returns an object describing the available fragment\n\t * directions.\n\t *\n\t * @return {{prev: boolean, next: boolean}}\n\t */\n\tavailableRoutes() {\n\n\t\tlet currentSlide = this.Reveal.getCurrentSlide();\n\t\tif( currentSlide && this.Reveal.getConfig().fragments ) {\n\t\t\tlet fragments = currentSlide.querySelectorAll( '.fragment:not(.disabled)' );\n\t\t\tlet hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.disabled):not(.visible)' );\n\n\t\t\treturn {\n\t\t\t\tprev: fragments.length - hiddenFragments.length > 0,\n\t\t\t\tnext: !!hiddenFragments.length\n\t\t\t};\n\t\t}\n\t\telse {\n\t\t\treturn { prev: false, next: false };\n\t\t}\n\n\t}\n\n\t/**\n\t * Return a sorted fragments list, ordered by an increasing\n\t * \"data-fragment-index\" attribute.\n\t *\n\t * Fragments will be revealed in the order that they are returned by\n\t * this function, so you can use the index attributes to control the\n\t * order of fragment appearance.\n\t *\n\t * To maintain a sensible default fragment order, fragments are presumed\n\t * to be passed in document order. This function adds a \"fragment-index\"\n\t * attribute to each node if such an attribute is not already present,\n\t * and sets that attribute to an integer value which is the position of\n\t * the fragment within the fragments list.\n\t *\n\t * @param {object[]|*} fragments\n\t * @param {boolean} grouped If true the returned array will contain\n\t * nested arrays for all fragments with the same index\n\t * @return {object[]} sorted Sorted array of fragments\n\t */\n\tsort( fragments, grouped = false ) {\n\n\t\tfragments = Array.from( fragments );\n\n\t\tlet ordered = [],\n\t\t\tunordered = [],\n\t\t\tsorted = [];\n\n\t\t// Group ordered and unordered elements\n\t\tfragments.forEach( fragment => {\n\t\t\tif( fragment.hasAttribute( 'data-fragment-index' ) ) {\n\t\t\t\tlet index = parseInt( fragment.getAttribute( 'data-fragment-index' ), 10 );\n\n\t\t\t\tif( !ordered[index] ) {\n\t\t\t\t\tordered[index] = [];\n\t\t\t\t}\n\n\t\t\t\tordered[index].push( fragment );\n\t\t\t}\n\t\t\telse {\n\t\t\t\tunordered.push( [ fragment ] );\n\t\t\t}\n\t\t} );\n\n\t\t// Append fragments without explicit indices in their\n\t\t// DOM order\n\t\tordered = ordered.concat( unordered );\n\n\t\t// Manually count the index up per group to ensure there\n\t\t// are no gaps\n\t\tlet index = 0;\n\n\t\t// Push all fragments in their sorted order to an array,\n\t\t// this flattens the groups\n\t\tordered.forEach( group => {\n\t\t\tgroup.forEach( fragment => {\n\t\t\t\tsorted.push( fragment );\n\t\t\t\tfragment.setAttribute( 'data-fragment-index', index );\n\t\t\t} );\n\n\t\t\tindex ++;\n\t\t} );\n\n\t\treturn grouped === true ? ordered : sorted;\n\n\t}\n\n\t/**\n\t * Sorts and formats all of fragments in the\n\t * presentation.\n\t */\n\tsortAll() {\n\n\t\tthis.Reveal.getHorizontalSlides().forEach( horizontalSlide => {\n\n\t\t\tlet verticalSlides = queryAll( horizontalSlide, 'section' );\n\t\t\tverticalSlides.forEach( ( verticalSlide, y ) => {\n\n\t\t\t\tthis.sort( verticalSlide.querySelectorAll( '.fragment' ) );\n\n\t\t\t}, this );\n\n\t\t\tif( verticalSlides.length === 0 ) this.sort( horizontalSlide.querySelectorAll( '.fragment' ) );\n\n\t\t} );\n\n\t}\n\n\t/**\n\t * Refreshes the fragments on the current slide so that they\n\t * have the appropriate classes (.visible + .current-fragment).\n\t *\n\t * @param {number} [index] The index of the current fragment\n\t * @param {array} [fragments] Array containing all fragments\n\t * in the current slide\n\t *\n\t * @return {{shown: array, hidden: array}}\n\t */\n\tupdate( index, fragments ) {\n\n\t\tlet changedFragments = {\n\t\t\tshown: [],\n\t\t\thidden: []\n\t\t};\n\n\t\tlet currentSlide = this.Reveal.getCurrentSlide();\n\t\tif( currentSlide && this.Reveal.getConfig().fragments ) {\n\n\t\t\tfragments = fragments || this.sort( currentSlide.querySelectorAll( '.fragment' ) );\n\n\t\t\tif( fragments.length ) {\n\n\t\t\t\tlet maxIndex = 0;\n\n\t\t\t\tif( typeof index !== 'number' ) {\n\t\t\t\t\tlet currentFragment = this.sort( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();\n\t\t\t\t\tif( currentFragment ) {\n\t\t\t\t\t\tindex = parseInt( currentFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tArray.from( fragments ).forEach( ( el, i ) => {\n\n\t\t\t\t\tif( el.hasAttribute( 'data-fragment-index' ) ) {\n\t\t\t\t\t\ti = parseInt( el.getAttribute( 'data-fragment-index' ), 10 );\n\t\t\t\t\t}\n\n\t\t\t\t\tmaxIndex = Math.max( maxIndex, i );\n\n\t\t\t\t\t// Visible fragments\n\t\t\t\t\tif( i <= index ) {\n\t\t\t\t\t\tlet wasVisible = el.classList.contains( 'visible' )\n\t\t\t\t\t\tel.classList.add( 'visible' );\n\t\t\t\t\t\tel.classList.remove( 'current-fragment' );\n\n\t\t\t\t\t\tif( i === index ) {\n\t\t\t\t\t\t\t// Announce the fragments one by one to the Screen Reader\n\t\t\t\t\t\t\tthis.Reveal.announceStatus( this.Reveal.getStatusText( el ) );\n\n\t\t\t\t\t\t\tel.classList.add( 'current-fragment' );\n\t\t\t\t\t\t\tthis.Reveal.slideContent.startEmbeddedContent( el );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif( !wasVisible ) {\n\t\t\t\t\t\t\tchangedFragments.shown.push( el )\n\t\t\t\t\t\t\tthis.Reveal.dispatchEvent({\n\t\t\t\t\t\t\t\ttarget: el,\n\t\t\t\t\t\t\t\ttype: 'visible',\n\t\t\t\t\t\t\t\tbubbles: false\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// Hidden fragments\n\t\t\t\t\telse {\n\t\t\t\t\t\tlet wasVisible = el.classList.contains( 'visible' )\n\t\t\t\t\t\tel.classList.remove( 'visible' );\n\t\t\t\t\t\tel.classList.remove( 'current-fragment' );\n\n\t\t\t\t\t\tif( wasVisible ) {\n\t\t\t\t\t\t\tthis.Reveal.slideContent.stopEmbeddedContent( el );\n\t\t\t\t\t\t\tchangedFragments.hidden.push( el );\n\t\t\t\t\t\t\tthis.Reveal.dispatchEvent({\n\t\t\t\t\t\t\t\ttarget: el,\n\t\t\t\t\t\t\t\ttype: 'hidden',\n\t\t\t\t\t\t\t\tbubbles: false\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t} );\n\n\t\t\t\t// Write the current fragment index to the slide
.\n\t\t\t\t// This can be used by end users to apply styles based on\n\t\t\t\t// the current fragment index.\n\t\t\t\tindex = typeof index === 'number' ? index : -1;\n\t\t\t\tindex = Math.max( Math.min( index, maxIndex ), -1 );\n\t\t\t\tcurrentSlide.setAttribute( 'data-fragment', index );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn changedFragments;\n\n\t}\n\n\t/**\n\t * Formats the fragments on the given slide so that they have\n\t * valid indices. Call this if fragments are changed in the DOM\n\t * after reveal.js has already initialized.\n\t *\n\t * @param {HTMLElement} slide\n\t * @return {Array} a list of the HTML fragments that were synced\n\t */\n\tsync( slide = this.Reveal.getCurrentSlide() ) {\n\n\t\treturn this.sort( slide.querySelectorAll( '.fragment' ) );\n\n\t}\n\n\t/**\n\t * Navigate to the specified slide fragment.\n\t *\n\t * @param {?number} index The index of the fragment that\n\t * should be shown, -1 means all are invisible\n\t * @param {number} offset Integer offset to apply to the\n\t * fragment index\n\t *\n\t * @return {boolean} true if a change was made in any\n\t * fragments visibility as part of this call\n\t */\n\tgoto( index, offset = 0 ) {\n\n\t\tlet currentSlide = this.Reveal.getCurrentSlide();\n\t\tif( currentSlide && this.Reveal.getConfig().fragments ) {\n\n\t\t\tlet fragments = this.sort( currentSlide.querySelectorAll( '.fragment:not(.disabled)' ) );\n\t\t\tif( fragments.length ) {\n\n\t\t\t\t// If no index is specified, find the current\n\t\t\t\tif( typeof index !== 'number' ) {\n\t\t\t\t\tlet lastVisibleFragment = this.sort( currentSlide.querySelectorAll( '.fragment:not(.disabled).visible' ) ).pop();\n\n\t\t\t\t\tif( lastVisibleFragment ) {\n\t\t\t\t\t\tindex = parseInt( lastVisibleFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tindex = -1;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Apply the offset if there is one\n\t\t\t\tindex += offset;\n\n\t\t\t\tlet changedFragments = this.update( index, fragments );\n\n\t\t\t\tif( changedFragments.hidden.length ) {\n\t\t\t\t\tthis.Reveal.dispatchEvent({\n\t\t\t\t\t\ttype: 'fragmenthidden',\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\tfragment: changedFragments.hidden[0],\n\t\t\t\t\t\t\tfragments: changedFragments.hidden\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tif( changedFragments.shown.length ) {\n\t\t\t\t\tthis.Reveal.dispatchEvent({\n\t\t\t\t\t\ttype: 'fragmentshown',\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\tfragment: changedFragments.shown[0],\n\t\t\t\t\t\t\tfragments: changedFragments.shown\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tthis.Reveal.controls.update();\n\t\t\t\tthis.Reveal.progress.update();\n\n\t\t\t\tif( this.Reveal.getConfig().fragmentInURL ) {\n\t\t\t\t\tthis.Reveal.location.writeURL();\n\t\t\t\t}\n\n\t\t\t\treturn !!( changedFragments.shown.length || changedFragments.hidden.length );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Navigate to the next slide fragment.\n\t *\n\t * @return {boolean} true if there was a next fragment,\n\t * false otherwise\n\t */\n\tnext() {\n\n\t\treturn this.goto( null, 1 );\n\n\t}\n\n\t/**\n\t * Navigate to the previous slide fragment.\n\t *\n\t * @return {boolean} true if there was a previous fragment,\n\t * false otherwise\n\t */\n\tprev() {\n\n\t\treturn this.goto( null, -1 );\n\n\t}\n\n}","import { SLIDES_SELECTOR } from '../utils/constants.js'\nimport { extend, queryAll, transformElement } from '../utils/util.js'\n\n/**\n * Handles all logic related to the overview mode\n * (birds-eye view of all slides).\n */\nexport default class Overview {\n\n\tconstructor( Reveal ) {\n\n\t\tthis.Reveal = Reveal;\n\n\t\tthis.active = false;\n\n\t\tthis.onSlideClicked = this.onSlideClicked.bind( this );\n\n\t}\n\n\t/**\n\t * Displays the overview of slides (quick nav) by scaling\n\t * down and arranging all slide elements.\n\t */\n\tactivate() {\n\n\t\t// Only proceed if enabled in config\n\t\tif( this.Reveal.getConfig().overview && !this.isActive() ) {\n\n\t\t\tthis.active = true;\n\n\t\t\tthis.Reveal.getRevealElement().classList.add( 'overview' );\n\n\t\t\t// Don't auto-slide while in overview mode\n\t\t\tthis.Reveal.cancelAutoSlide();\n\n\t\t\t// Move the backgrounds element into the slide container to\n\t\t\t// that the same scaling is applied\n\t\t\tthis.Reveal.getSlidesElement().appendChild( this.Reveal.getBackgroundsElement() );\n\n\t\t\t// Clicking on an overview slide navigates to it\n\t\t\tqueryAll( this.Reveal.getRevealElement(), SLIDES_SELECTOR ).forEach( slide => {\n\t\t\t\tif( !slide.classList.contains( 'stack' ) ) {\n\t\t\t\t\tslide.addEventListener( 'click', this.onSlideClicked, true );\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\t// Calculate slide sizes\n\t\t\tconst margin = 70;\n\t\t\tconst slideSize = this.Reveal.getComputedSlideSize();\n\t\t\tthis.overviewSlideWidth = slideSize.width + margin;\n\t\t\tthis.overviewSlideHeight = slideSize.height + margin;\n\n\t\t\t// Reverse in RTL mode\n\t\t\tif( this.Reveal.getConfig().rtl ) {\n\t\t\t\tthis.overviewSlideWidth = -this.overviewSlideWidth;\n\t\t\t}\n\n\t\t\tthis.Reveal.updateSlidesVisibility();\n\n\t\t\tthis.layout();\n\t\t\tthis.update();\n\n\t\t\tthis.Reveal.layout();\n\n\t\t\tconst indices = this.Reveal.getIndices();\n\n\t\t\t// Notify observers of the overview showing\n\t\t\tthis.Reveal.dispatchEvent({\n\t\t\t\ttype: 'overviewshown',\n\t\t\t\tdata: {\n\t\t\t\t\t'indexh': indices.h,\n\t\t\t\t\t'indexv': indices.v,\n\t\t\t\t\t'currentSlide': this.Reveal.getCurrentSlide()\n\t\t\t\t}\n\t\t\t});\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Uses CSS transforms to position all slides in a grid for\n\t * display inside of the overview mode.\n\t */\n\tlayout() {\n\n\t\t// Layout slides\n\t\tthis.Reveal.getHorizontalSlides().forEach( ( hslide, h ) => {\n\t\t\thslide.setAttribute( 'data-index-h', h );\n\t\t\ttransformElement( hslide, 'translate3d(' + ( h * this.overviewSlideWidth ) + 'px, 0, 0)' );\n\n\t\t\tif( hslide.classList.contains( 'stack' ) ) {\n\n\t\t\t\tqueryAll( hslide, 'section' ).forEach( ( vslide, v ) => {\n\t\t\t\t\tvslide.setAttribute( 'data-index-h', h );\n\t\t\t\t\tvslide.setAttribute( 'data-index-v', v );\n\n\t\t\t\t\ttransformElement( vslide, 'translate3d(0, ' + ( v * this.overviewSlideHeight ) + 'px, 0)' );\n\t\t\t\t} );\n\n\t\t\t}\n\t\t} );\n\n\t\t// Layout slide backgrounds\n\t\tArray.from( this.Reveal.getBackgroundsElement().childNodes ).forEach( ( hbackground, h ) => {\n\t\t\ttransformElement( hbackground, 'translate3d(' + ( h * this.overviewSlideWidth ) + 'px, 0, 0)' );\n\n\t\t\tqueryAll( hbackground, '.slide-background' ).forEach( ( vbackground, v ) => {\n\t\t\t\ttransformElement( vbackground, 'translate3d(0, ' + ( v * this.overviewSlideHeight ) + 'px, 0)' );\n\t\t\t} );\n\t\t} );\n\n\t}\n\n\t/**\n\t * Moves the overview viewport to the current slides.\n\t * Called each time the current slide changes.\n\t */\n\tupdate() {\n\n\t\tconst vmin = Math.min( window.innerWidth, window.innerHeight );\n\t\tconst scale = Math.max( vmin / 5, 150 ) / vmin;\n\t\tconst indices = this.Reveal.getIndices();\n\n\t\tthis.Reveal.transformSlides( {\n\t\t\toverview: [\n\t\t\t\t'scale('+ scale +')',\n\t\t\t\t'translateX('+ ( -indices.h * this.overviewSlideWidth ) +'px)',\n\t\t\t\t'translateY('+ ( -indices.v * this.overviewSlideHeight ) +'px)'\n\t\t\t].join( ' ' )\n\t\t} );\n\n\t}\n\n\t/**\n\t * Exits the slide overview and enters the currently\n\t * active slide.\n\t */\n\tdeactivate() {\n\n\t\t// Only proceed if enabled in config\n\t\tif( this.Reveal.getConfig().overview ) {\n\n\t\t\tthis.active = false;\n\n\t\t\tthis.Reveal.getRevealElement().classList.remove( 'overview' );\n\n\t\t\t// Temporarily add a class so that transitions can do different things\n\t\t\t// depending on whether they are exiting/entering overview, or just\n\t\t\t// moving from slide to slide\n\t\t\tthis.Reveal.getRevealElement().classList.add( 'overview-deactivating' );\n\n\t\t\tsetTimeout( () => {\n\t\t\t\tthis.Reveal.getRevealElement().classList.remove( 'overview-deactivating' );\n\t\t\t}, 1 );\n\n\t\t\t// Move the background element back out\n\t\t\tthis.Reveal.getRevealElement().appendChild( this.Reveal.getBackgroundsElement() );\n\n\t\t\t// Clean up changes made to slides\n\t\t\tqueryAll( this.Reveal.getRevealElement(), SLIDES_SELECTOR ).forEach( slide => {\n\t\t\t\ttransformElement( slide, '' );\n\n\t\t\t\tslide.removeEventListener( 'click', this.onSlideClicked, true );\n\t\t\t} );\n\n\t\t\t// Clean up changes made to backgrounds\n\t\t\tqueryAll( this.Reveal.getBackgroundsElement(), '.slide-background' ).forEach( background => {\n\t\t\t\ttransformElement( background, '' );\n\t\t\t} );\n\n\t\t\tthis.Reveal.transformSlides( { overview: '' } );\n\n\t\t\tconst indices = this.Reveal.getIndices();\n\n\t\t\tthis.Reveal.slide( indices.h, indices.v );\n\t\t\tthis.Reveal.layout();\n\t\t\tthis.Reveal.cueAutoSlide();\n\n\t\t\t// Notify observers of the overview hiding\n\t\t\tthis.Reveal.dispatchEvent({\n\t\t\t\ttype: 'overviewhidden',\n\t\t\t\tdata: {\n\t\t\t\t\t'indexh': indices.h,\n\t\t\t\t\t'indexv': indices.v,\n\t\t\t\t\t'currentSlide': this.Reveal.getCurrentSlide()\n\t\t\t\t}\n\t\t\t});\n\n\t\t}\n\t}\n\n\t/**\n\t * Toggles the slide overview mode on and off.\n\t *\n\t * @param {Boolean} [override] Flag which overrides the\n\t * toggle logic and forcibly sets the desired state. True means\n\t * overview is open, false means it's closed.\n\t */\n\ttoggle( override ) {\n\n\t\tif( typeof override === 'boolean' ) {\n\t\t\toverride ? this.activate() : this.deactivate();\n\t\t}\n\t\telse {\n\t\t\tthis.isActive() ? this.deactivate() : this.activate();\n\t\t}\n\n\t}\n\n\t/**\n\t * Checks if the overview is currently active.\n\t *\n\t * @return {Boolean} true if the overview is active,\n\t * false otherwise\n\t */\n\tisActive() {\n\n\t\treturn this.active;\n\n\t}\n\n\t/**\n\t * Invoked when a slide is and we're in the overview.\n\t *\n\t * @param {object} event\n\t */\n\tonSlideClicked( event ) {\n\n\t\tif( this.isActive() ) {\n\t\t\tevent.preventDefault();\n\n\t\t\tlet element = event.target;\n\n\t\t\twhile( element && !element.nodeName.match( /section/gi ) ) {\n\t\t\t\telement = element.parentNode;\n\t\t\t}\n\n\t\t\tif( element && !element.classList.contains( 'disabled' ) ) {\n\n\t\t\t\tthis.deactivate();\n\n\t\t\t\tif( element.nodeName.match( /section/gi ) ) {\n\t\t\t\t\tlet h = parseInt( element.getAttribute( 'data-index-h' ), 10 ),\n\t\t\t\t\t\tv = parseInt( element.getAttribute( 'data-index-v' ), 10 );\n\n\t\t\t\t\tthis.Reveal.slide( h, v );\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\n\t}\n\n}","import { enterFullscreen } from '../utils/util.js'\n\n/**\n * Handles all reveal.js keyboard interactions.\n */\nexport default class Keyboard {\n\n\tconstructor( Reveal ) {\n\n\t\tthis.Reveal = Reveal;\n\n\t\t// A key:value map of keyboard keys and descriptions of\n\t\t// the actions they trigger\n\t\tthis.shortcuts = {};\n\n\t\t// Holds custom key code mappings\n\t\tthis.bindings = {};\n\n\t\tthis.onDocumentKeyDown = this.onDocumentKeyDown.bind( this );\n\t\tthis.onDocumentKeyPress = this.onDocumentKeyPress.bind( this );\n\n\t}\n\n\t/**\n\t * Called when the reveal.js config is updated.\n\t */\n\tconfigure( config, oldConfig ) {\n\n\t\tif( config.navigationMode === 'linear' ) {\n\t\t\tthis.shortcuts['→ , ↓ , SPACE , N , L , J'] = 'Next slide';\n\t\t\tthis.shortcuts['← , ↑ , P , H , K'] = 'Previous slide';\n\t\t}\n\t\telse {\n\t\t\tthis.shortcuts['N , SPACE'] = 'Next slide';\n\t\t\tthis.shortcuts['P , Shift SPACE'] = 'Previous slide';\n\t\t\tthis.shortcuts['← , H'] = 'Navigate left';\n\t\t\tthis.shortcuts['→ , L'] = 'Navigate right';\n\t\t\tthis.shortcuts['↑ , K'] = 'Navigate up';\n\t\t\tthis.shortcuts['↓ , J'] = 'Navigate down';\n\t\t}\n\n\t\tthis.shortcuts['Alt + ←/↑/→/↓'] = 'Navigate without fragments';\n\t\tthis.shortcuts['Shift + ←/↑/→/↓'] = 'Jump to first/last slide';\n\t\tthis.shortcuts['B , .'] = 'Pause';\n\t\tthis.shortcuts['F'] = 'Fullscreen';\n\t\tthis.shortcuts['ESC, O'] = 'Slide overview';\n\n\t}\n\n\t/**\n\t * Starts listening for keyboard events.\n\t */\n\tbind() {\n\n\t\tdocument.addEventListener( 'keydown', this.onDocumentKeyDown, false );\n\t\tdocument.addEventListener( 'keypress', this.onDocumentKeyPress, false );\n\n\t}\n\n\t/**\n\t * Stops listening for keyboard events.\n\t */\n\tunbind() {\n\n\t\tdocument.removeEventListener( 'keydown', this.onDocumentKeyDown, false );\n\t\tdocument.removeEventListener( 'keypress', this.onDocumentKeyPress, false );\n\n\t}\n\n\t/**\n\t * Add a custom key binding with optional description to\n\t * be added to the help screen.\n\t */\n\taddKeyBinding( binding, callback ) {\n\n\t\tif( typeof binding === 'object' && binding.keyCode ) {\n\t\t\tthis.bindings[binding.keyCode] = {\n\t\t\t\tcallback: callback,\n\t\t\t\tkey: binding.key,\n\t\t\t\tdescription: binding.description\n\t\t\t};\n\t\t}\n\t\telse {\n\t\t\tthis.bindings[binding] = {\n\t\t\t\tcallback: callback,\n\t\t\t\tkey: null,\n\t\t\t\tdescription: null\n\t\t\t};\n\t\t}\n\n\t}\n\n\t/**\n\t * Removes the specified custom key binding.\n\t */\n\tremoveKeyBinding( keyCode ) {\n\n\t\tdelete this.bindings[keyCode];\n\n\t}\n\n\t/**\n\t * Programmatically triggers a keyboard event\n\t *\n\t * @param {int} keyCode\n\t */\n\ttriggerKey( keyCode ) {\n\n\t\tthis.onDocumentKeyDown( { keyCode } );\n\n\t}\n\n\t/**\n\t * Registers a new shortcut to include in the help overlay\n\t *\n\t * @param {String} key\n\t * @param {String} value\n\t */\n\tregisterKeyboardShortcut( key, value ) {\n\n\t\tthis.shortcuts[key] = value;\n\n\t}\n\n\tgetShortcuts() {\n\n\t\treturn this.shortcuts;\n\n\t}\n\n\tgetBindings() {\n\n\t\treturn this.bindings;\n\n\t}\n\n\t/**\n\t * Handler for the document level 'keypress' event.\n\t *\n\t * @param {object} event\n\t */\n\tonDocumentKeyPress( event ) {\n\n\t\t// Check if the pressed key is question mark\n\t\tif( event.shiftKey && event.charCode === 63 ) {\n\t\t\tthis.Reveal.toggleHelp();\n\t\t}\n\n\t}\n\n\t/**\n\t * Handler for the document level 'keydown' event.\n\t *\n\t * @param {object} event\n\t */\n\tonDocumentKeyDown( event ) {\n\n\t\tlet config = this.Reveal.getConfig();\n\n\t\t// If there's a condition specified and it returns false,\n\t\t// ignore this event\n\t\tif( typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// If keyboardCondition is set, only capture keyboard events\n\t\t// for embedded decks when they are focused\n\t\tif( config.keyboardCondition === 'focused' && !this.Reveal.isFocused() ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Shorthand\n\t\tlet keyCode = event.keyCode;\n\n\t\t// Remember if auto-sliding was paused so we can toggle it\n\t\tlet autoSlideWasPaused = !this.Reveal.isAutoSliding();\n\n\t\tthis.Reveal.onUserInput( event );\n\n\t\t// Is there a focused element that could be using the keyboard?\n\t\tlet activeElementIsCE = document.activeElement && document.activeElement.isContentEditable === true;\n\t\tlet activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );\n\t\tlet activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);\n\n\t\t// Whitelist certain modifiers for slide navigation shortcuts\n\t\tlet isNavigationKey = [32, 37, 38, 39, 40, 78, 80].indexOf( event.keyCode ) !== -1;\n\n\t\t// Prevent all other events when a modifier is pressed\n\t\tlet unusedModifier = \t!( isNavigationKey && event.shiftKey || event.altKey ) &&\n\t\t\t\t\t\t\t\t( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );\n\n\t\t// Disregard the event if there's a focused element or a\n\t\t// keyboard modifier key is present\n\t\tif( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;\n\n\t\t// While paused only allow resume keyboard events; 'b', 'v', '.'\n\t\tlet resumeKeyCodes = [66,86,190,191];\n\t\tlet key;\n\n\t\t// Custom key bindings for togglePause should be able to resume\n\t\tif( typeof config.keyboard === 'object' ) {\n\t\t\tfor( key in config.keyboard ) {\n\t\t\t\tif( config.keyboard[key] === 'togglePause' ) {\n\t\t\t\t\tresumeKeyCodes.push( parseInt( key, 10 ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif( this.Reveal.isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Use linear navigation if we're configured to OR if\n\t\t// the presentation is one-dimensional\n\t\tlet useLinearMode = config.navigationMode === 'linear' || !this.Reveal.hasHorizontalSlides() || !this.Reveal.hasVerticalSlides();\n\n\t\tlet triggered = false;\n\n\t\t// 1. User defined key bindings\n\t\tif( typeof config.keyboard === 'object' ) {\n\n\t\t\tfor( key in config.keyboard ) {\n\n\t\t\t\t// Check if this binding matches the pressed key\n\t\t\t\tif( parseInt( key, 10 ) === keyCode ) {\n\n\t\t\t\t\tlet value = config.keyboard[ key ];\n\n\t\t\t\t\t// Callback function\n\t\t\t\t\tif( typeof value === 'function' ) {\n\t\t\t\t\t\tvalue.apply( null, [ event ] );\n\t\t\t\t\t}\n\t\t\t\t\t// String shortcuts to reveal.js API\n\t\t\t\t\telse if( typeof value === 'string' && typeof this.Reveal[ value ] === 'function' ) {\n\t\t\t\t\t\tthis.Reveal[ value ].call();\n\t\t\t\t\t}\n\n\t\t\t\t\ttriggered = true;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\t// 2. Registered custom key bindings\n\t\tif( triggered === false ) {\n\n\t\t\tfor( key in this.bindings ) {\n\n\t\t\t\t// Check if this binding matches the pressed key\n\t\t\t\tif( parseInt( key, 10 ) === keyCode ) {\n\n\t\t\t\t\tlet action = this.bindings[ key ].callback;\n\n\t\t\t\t\t// Callback function\n\t\t\t\t\tif( typeof action === 'function' ) {\n\t\t\t\t\t\taction.apply( null, [ event ] );\n\t\t\t\t\t}\n\t\t\t\t\t// String shortcuts to reveal.js API\n\t\t\t\t\telse if( typeof action === 'string' && typeof this.Reveal[ action ] === 'function' ) {\n\t\t\t\t\t\tthis.Reveal[ action ].call();\n\t\t\t\t\t}\n\n\t\t\t\t\ttriggered = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// 3. System defined key bindings\n\t\tif( triggered === false ) {\n\n\t\t\t// Assume true and try to prove false\n\t\t\ttriggered = true;\n\n\t\t\t// P, PAGE UP\n\t\t\tif( keyCode === 80 || keyCode === 33 ) {\n\t\t\t\tthis.Reveal.prev({skipFragments: event.altKey});\n\t\t\t}\n\t\t\t// N, PAGE DOWN\n\t\t\telse if( keyCode === 78 || keyCode === 34 ) {\n\t\t\t\tthis.Reveal.next({skipFragments: event.altKey});\n\t\t\t}\n\t\t\t// H, LEFT\n\t\t\telse if( keyCode === 72 || keyCode === 37 ) {\n\t\t\t\tif( event.shiftKey ) {\n\t\t\t\t\tthis.Reveal.slide( 0 );\n\t\t\t\t}\n\t\t\t\telse if( !this.Reveal.overview.isActive() && useLinearMode ) {\n\t\t\t\t\tthis.Reveal.prev({skipFragments: event.altKey});\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tthis.Reveal.left({skipFragments: event.altKey});\n\t\t\t\t}\n\t\t\t}\n\t\t\t// L, RIGHT\n\t\t\telse if( keyCode === 76 || keyCode === 39 ) {\n\t\t\t\tif( event.shiftKey ) {\n\t\t\t\t\tthis.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );\n\t\t\t\t}\n\t\t\t\telse if( !this.Reveal.overview.isActive() && useLinearMode ) {\n\t\t\t\t\tthis.Reveal.next({skipFragments: event.altKey});\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tthis.Reveal.right({skipFragments: event.altKey});\n\t\t\t\t}\n\t\t\t}\n\t\t\t// K, UP\n\t\t\telse if( keyCode === 75 || keyCode === 38 ) {\n\t\t\t\tif( event.shiftKey ) {\n\t\t\t\t\tthis.Reveal.slide( undefined, 0 );\n\t\t\t\t}\n\t\t\t\telse if( !this.Reveal.overview.isActive() && useLinearMode ) {\n\t\t\t\t\tthis.Reveal.prev({skipFragments: event.altKey});\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tthis.Reveal.up({skipFragments: event.altKey});\n\t\t\t\t}\n\t\t\t}\n\t\t\t// J, DOWN\n\t\t\telse if( keyCode === 74 || keyCode === 40 ) {\n\t\t\t\tif( event.shiftKey ) {\n\t\t\t\t\tthis.Reveal.slide( undefined, Number.MAX_VALUE );\n\t\t\t\t}\n\t\t\t\telse if( !this.Reveal.overview.isActive() && useLinearMode ) {\n\t\t\t\t\tthis.Reveal.next({skipFragments: event.altKey});\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tthis.Reveal.down({skipFragments: event.altKey});\n\t\t\t\t}\n\t\t\t}\n\t\t\t// HOME\n\t\t\telse if( keyCode === 36 ) {\n\t\t\t\tthis.Reveal.slide( 0 );\n\t\t\t}\n\t\t\t// END\n\t\t\telse if( keyCode === 35 ) {\n\t\t\t\tthis.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );\n\t\t\t}\n\t\t\t// SPACE\n\t\t\telse if( keyCode === 32 ) {\n\t\t\t\tif( this.Reveal.overview.isActive() ) {\n\t\t\t\t\tthis.Reveal.overview.deactivate();\n\t\t\t\t}\n\t\t\t\tif( event.shiftKey ) {\n\t\t\t\t\tthis.Reveal.prev({skipFragments: event.altKey});\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tthis.Reveal.next({skipFragments: event.altKey});\n\t\t\t\t}\n\t\t\t}\n\t\t\t// TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS \"BLACK SCREEN\" BUTTON\n\t\t\telse if( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) {\n\t\t\t\tthis.Reveal.togglePause();\n\t\t\t}\n\t\t\t// F\n\t\t\telse if( keyCode === 70 ) {\n\t\t\t\tenterFullscreen( config.embedded ? this.Reveal.getViewportElement() : document.documentElement );\n\t\t\t}\n\t\t\t// A\n\t\t\telse if( keyCode === 65 ) {\n\t\t\t\tif ( config.autoSlideStoppable ) {\n\t\t\t\t\tthis.Reveal.toggleAutoSlide( autoSlideWasPaused );\n\t\t\t\t}\n\t\t\t}\n\t\t\telse {\n\t\t\t\ttriggered = false;\n\t\t\t}\n\n\t\t}\n\n\t\t// If the input resulted in a triggered action we should prevent\n\t\t// the browsers default behavior\n\t\tif( triggered ) {\n\t\t\tevent.preventDefault && event.preventDefault();\n\t\t}\n\t\t// ESC or O key\n\t\telse if( keyCode === 27 || keyCode === 79 ) {\n\t\t\tif( this.Reveal.closeOverlay() === false ) {\n\t\t\t\tthis.Reveal.overview.toggle();\n\t\t\t}\n\n\t\t\tevent.preventDefault && event.preventDefault();\n\t\t}\n\n\t\t// If auto-sliding is enabled we need to cue up\n\t\t// another timeout\n\t\tthis.Reveal.cueAutoSlide();\n\n\t}\n\n}","/**\n * Reads and writes the URL based on reveal.js' current state.\n */\nexport default class Location {\n\n\t// The minimum number of milliseconds that must pass between\n\t// calls to history.replaceState\n\tMAX_REPLACE_STATE_FREQUENCY = 1000\n\n\tconstructor( Reveal ) {\n\n\t\tthis.Reveal = Reveal;\n\n\t\t// Delays updates to the URL due to a Chrome thumbnailer bug\n\t\tthis.writeURLTimeout = 0;\n\n\t\tthis.replaceStateTimestamp = 0;\n\n\t\tthis.onWindowHashChange = this.onWindowHashChange.bind( this );\n\n\t}\n\n\tbind() {\n\n\t\twindow.addEventListener( 'hashchange', this.onWindowHashChange, false );\n\n\t}\n\n\tunbind() {\n\n\t\twindow.removeEventListener( 'hashchange', this.onWindowHashChange, false );\n\n\t}\n\n\t/**\n\t * Returns the slide indices for the given hash link.\n\t *\n\t * @param {string} [hash] the hash string that we want to\n\t * find the indices for\n\t *\n\t * @returns slide indices or null\n\t */\n\tgetIndicesFromHash( hash=window.location.hash ) {\n\n\t\t// Attempt to parse the hash as either an index or name\n\t\tlet name = hash.replace( /^#\\/?/, '' );\n\t\tlet bits = name.split( '/' );\n\n\t\t// If the first bit is not fully numeric and there is a name we\n\t\t// can assume that this is a named link\n\t\tif( !/^[0-9]*$/.test( bits[0] ) && name.length ) {\n\t\t\tlet element;\n\n\t\t\tlet f;\n\n\t\t\t// Parse named links with fragments (#/named-link/2)\n\t\t\tif( /\\/[-\\d]+$/g.test( name ) ) {\n\t\t\t\tf = parseInt( name.split( '/' ).pop(), 10 );\n\t\t\t\tf = isNaN(f) ? undefined : f;\n\t\t\t\tname = name.split( '/' ).shift();\n\t\t\t}\n\n\t\t\t// Ensure the named link is a valid HTML ID attribute\n\t\t\ttry {\n\t\t\t\telement = document.getElementById( decodeURIComponent( name ) );\n\t\t\t}\n\t\t\tcatch ( error ) { }\n\n\t\t\tif( element ) {\n\t\t\t\treturn { ...this.Reveal.getIndices( element ), f };\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tconst config = this.Reveal.getConfig();\n\t\t\tlet hashIndexBase = config.hashOneBasedIndex ? 1 : 0;\n\n\t\t\t// Read the index components of the hash\n\t\t\tlet h = ( parseInt( bits[0], 10 ) - hashIndexBase ) || 0,\n\t\t\t\tv = ( parseInt( bits[1], 10 ) - hashIndexBase ) || 0,\n\t\t\t\tf;\n\n\t\t\tif( config.fragmentInURL ) {\n\t\t\t\tf = parseInt( bits[2], 10 );\n\t\t\t\tif( isNaN( f ) ) {\n\t\t\t\t\tf = undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { h, v, f };\n\t\t}\n\n\t\t// The hash couldn't be parsed or no matching named link was found\n\t\treturn null\n\n\t}\n\n\t/**\n\t * Reads the current URL (hash) and navigates accordingly.\n\t */\n\treadURL() {\n\n\t\tconst currentIndices = this.Reveal.getIndices();\n\t\tconst newIndices = this.getIndicesFromHash();\n\n\t\tif( newIndices ) {\n\t\t\tif( ( newIndices.h !== currentIndices.h || newIndices.v !== currentIndices.v || newIndices.f !== undefined ) ) {\n\t\t\t\t\tthis.Reveal.slide( newIndices.h, newIndices.v, newIndices.f );\n\t\t\t}\n\t\t}\n\t\t// If no new indices are available, we're trying to navigate to\n\t\t// a slide hash that does not exist\n\t\telse {\n\t\t\tthis.Reveal.slide( currentIndices.h || 0, currentIndices.v || 0 );\n\t\t}\n\n\t}\n\n\t/**\n\t * Updates the page URL (hash) to reflect the current\n\t * state.\n\t *\n\t * @param {number} delay The time in ms to wait before\n\t * writing the hash\n\t */\n\twriteURL( delay ) {\n\n\t\tlet config = this.Reveal.getConfig();\n\t\tlet currentSlide = this.Reveal.getCurrentSlide();\n\n\t\t// Make sure there's never more than one timeout running\n\t\tclearTimeout( this.writeURLTimeout );\n\n\t\t// If a delay is specified, timeout this call\n\t\tif( typeof delay === 'number' ) {\n\t\t\tthis.writeURLTimeout = setTimeout( this.writeURL, delay );\n\t\t}\n\t\telse if( currentSlide ) {\n\n\t\t\tlet hash = this.getHash();\n\n\t\t\t// If we're configured to push to history OR the history\n\t\t\t// API is not avaialble.\n\t\t\tif( config.history ) {\n\t\t\t\twindow.location.hash = hash;\n\t\t\t}\n\t\t\t// If we're configured to reflect the current slide in the\n\t\t\t// URL without pushing to history.\n\t\t\telse if( config.hash ) {\n\t\t\t\t// If the hash is empty, don't add it to the URL\n\t\t\t\tif( hash === '/' ) {\n\t\t\t\t\tthis.debouncedReplaceState( window.location.pathname + window.location.search );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tthis.debouncedReplaceState( '#' + hash );\n\t\t\t\t}\n\t\t\t}\n\t\t\t// UPDATE: The below nuking of all hash changes breaks\n\t\t\t// anchors on pages where reveal.js is running. Removed\n\t\t\t// in 4.0. Why was it here in the first place? ¯\\_(ツ)_/¯\n\t\t\t//\n\t\t\t// If history and hash are both disabled, a hash may still\n\t\t\t// be added to the URL by clicking on a href with a hash\n\t\t\t// target. Counter this by always removing the hash.\n\t\t\t// else {\n\t\t\t// \twindow.history.replaceState( null, null, window.location.pathname + window.location.search );\n\t\t\t// }\n\n\t\t}\n\n\t}\n\n\treplaceState( url ) {\n\n\t\twindow.history.replaceState( null, null, url );\n\t\tthis.replaceStateTimestamp = Date.now();\n\n\t}\n\n\tdebouncedReplaceState( url ) {\n\n\t\tclearTimeout( this.replaceStateTimeout );\n\n\t\tif( Date.now() - this.replaceStateTimestamp > this.MAX_REPLACE_STATE_FREQUENCY ) {\n\t\t\tthis.replaceState( url );\n\t\t}\n\t\telse {\n\t\t\tthis.replaceStateTimeout = setTimeout( () => this.replaceState( url ), this.MAX_REPLACE_STATE_FREQUENCY );\n\t\t}\n\n\t}\n\n\t/**\n\t * Return a hash URL that will resolve to the given slide location.\n\t *\n\t * @param {HTMLElement} [slide=currentSlide] The slide to link to\n\t */\n\tgetHash( slide ) {\n\n\t\tlet url = '/';\n\n\t\t// Attempt to create a named link based on the slide's ID\n\t\tlet s = slide || this.Reveal.getCurrentSlide();\n\t\tlet id = s ? s.getAttribute( 'id' ) : null;\n\t\tif( id ) {\n\t\t\tid = encodeURIComponent( id );\n\t\t}\n\n\t\tlet index = this.Reveal.getIndices( slide );\n\t\tif( !this.Reveal.getConfig().fragmentInURL ) {\n\t\t\tindex.f = undefined;\n\t\t}\n\n\t\t// If the current slide has an ID, use that as a named link,\n\t\t// but we don't support named links with a fragment index\n\t\tif( typeof id === 'string' && id.length ) {\n\t\t\turl = '/' + id;\n\n\t\t\t// If there is also a fragment, append that at the end\n\t\t\t// of the named link, like: #/named-link/2\n\t\t\tif( index.f >= 0 ) url += '/' + index.f;\n\t\t}\n\t\t// Otherwise use the /h/v index\n\t\telse {\n\t\t\tlet hashIndexBase = this.Reveal.getConfig().hashOneBasedIndex ? 1 : 0;\n\t\t\tif( index.h > 0 || index.v > 0 || index.f >= 0 ) url += index.h + hashIndexBase;\n\t\t\tif( index.v > 0 || index.f >= 0 ) url += '/' + (index.v + hashIndexBase );\n\t\t\tif( index.f >= 0 ) url += '/' + index.f;\n\t\t}\n\n\t\treturn url;\n\n\t}\n\n\t/**\n\t * Handler for the window level 'hashchange' event.\n\t *\n\t * @param {object} [event]\n\t */\n\tonWindowHashChange( event ) {\n\n\t\tthis.readURL();\n\n\t}\n\n}","import { queryAll } from '../utils/util.js'\nimport { isAndroid } from '../utils/device.js'\n\n/**\n * Manages our presentation controls. This includes both\n * the built-in control arrows as well as event monitoring\n * of any elements within the presentation with either of the\n * following helper classes:\n * - .navigate-up\n * - .navigate-right\n * - .navigate-down\n * - .navigate-left\n * - .navigate-next\n * - .navigate-prev\n */\nexport default class Controls {\n\n\tconstructor( Reveal ) {\n\n\t\tthis.Reveal = Reveal;\n\n\t\tthis.onNavigateLeftClicked = this.onNavigateLeftClicked.bind( this );\n\t\tthis.onNavigateRightClicked = this.onNavigateRightClicked.bind( this );\n\t\tthis.onNavigateUpClicked = this.onNavigateUpClicked.bind( this );\n\t\tthis.onNavigateDownClicked = this.onNavigateDownClicked.bind( this );\n\t\tthis.onNavigatePrevClicked = this.onNavigatePrevClicked.bind( this );\n\t\tthis.onNavigateNextClicked = this.onNavigateNextClicked.bind( this );\n\n\t}\n\n\trender() {\n\n\t\tconst rtl = this.Reveal.getConfig().rtl;\n\t\tconst revealElement = this.Reveal.getRevealElement();\n\n\t\tthis.element = document.createElement( 'aside' );\n\t\tthis.element.className = 'controls';\n\t\tthis.element.innerHTML =\n\t\t\t`\n\t\t\t\n\t\t\t\n\t\t\t`;\n\n\t\tthis.Reveal.getRevealElement().appendChild( this.element );\n\n\t\t// There can be multiple instances of controls throughout the page\n\t\tthis.controlsLeft = queryAll( revealElement, '.navigate-left' );\n\t\tthis.controlsRight = queryAll( revealElement, '.navigate-right' );\n\t\tthis.controlsUp = queryAll( revealElement, '.navigate-up' );\n\t\tthis.controlsDown = queryAll( revealElement, '.navigate-down' );\n\t\tthis.controlsPrev = queryAll( revealElement, '.navigate-prev' );\n\t\tthis.controlsNext = queryAll( revealElement, '.navigate-next' );\n\n\t\t// The left, right and down arrows in the standard reveal.js controls\n\t\tthis.controlsRightArrow = this.element.querySelector( '.navigate-right' );\n\t\tthis.controlsLeftArrow = this.element.querySelector( '.navigate-left' );\n\t\tthis.controlsDownArrow = this.element.querySelector( '.navigate-down' );\n\n\t}\n\n\t/**\n\t * Called when the reveal.js config is updated.\n\t */\n\tconfigure( config, oldConfig ) {\n\n\t\tthis.element.style.display = config.controls ? 'block' : 'none';\n\n\t\tthis.element.setAttribute( 'data-controls-layout', config.controlsLayout );\n\t\tthis.element.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows );\n\n\t}\n\n\tbind() {\n\n\t\t// Listen to both touch and click events, in case the device\n\t\t// supports both\n\t\tlet pointerEvents = [ 'touchstart', 'click' ];\n\n\t\t// Only support touch for Android, fixes double navigations in\n\t\t// stock browser\n\t\tif( isAndroid ) {\n\t\t\tpointerEvents = [ 'touchstart' ];\n\t\t}\n\n\t\tpointerEvents.forEach( eventName => {\n\t\t\tthis.controlsLeft.forEach( el => el.addEventListener( eventName, this.onNavigateLeftClicked, false ) );\n\t\t\tthis.controlsRight.forEach( el => el.addEventListener( eventName, this.onNavigateRightClicked, false ) );\n\t\t\tthis.controlsUp.forEach( el => el.addEventListener( eventName, this.onNavigateUpClicked, false ) );\n\t\t\tthis.controlsDown.forEach( el => el.addEventListener( eventName, this.onNavigateDownClicked, false ) );\n\t\t\tthis.controlsPrev.forEach( el => el.addEventListener( eventName, this.onNavigatePrevClicked, false ) );\n\t\t\tthis.controlsNext.forEach( el => el.addEventListener( eventName, this.onNavigateNextClicked, false ) );\n\t\t} );\n\n\t}\n\n\tunbind() {\n\n\t\t[ 'touchstart', 'click' ].forEach( eventName => {\n\t\t\tthis.controlsLeft.forEach( el => el.removeEventListener( eventName, this.onNavigateLeftClicked, false ) );\n\t\t\tthis.controlsRight.forEach( el => el.removeEventListener( eventName, this.onNavigateRightClicked, false ) );\n\t\t\tthis.controlsUp.forEach( el => el.removeEventListener( eventName, this.onNavigateUpClicked, false ) );\n\t\t\tthis.controlsDown.forEach( el => el.removeEventListener( eventName, this.onNavigateDownClicked, false ) );\n\t\t\tthis.controlsPrev.forEach( el => el.removeEventListener( eventName, this.onNavigatePrevClicked, false ) );\n\t\t\tthis.controlsNext.forEach( el => el.removeEventListener( eventName, this.onNavigateNextClicked, false ) );\n\t\t} );\n\n\t}\n\n\t/**\n\t * Updates the state of all control/navigation arrows.\n\t */\n\tupdate() {\n\n\t\tlet routes = this.Reveal.availableRoutes();\n\n\t\t// Remove the 'enabled' class from all directions\n\t\t[...this.controlsLeft, ...this.controlsRight, ...this.controlsUp, ...this.controlsDown, ...this.controlsPrev, ...this.controlsNext].forEach( node => {\n\t\t\tnode.classList.remove( 'enabled', 'fragmented' );\n\n\t\t\t// Set 'disabled' attribute on all directions\n\t\t\tnode.setAttribute( 'disabled', 'disabled' );\n\t\t} );\n\n\t\t// Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons\n\t\tif( routes.left ) this.controlsLeft.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );\n\t\tif( routes.right ) this.controlsRight.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );\n\t\tif( routes.up ) this.controlsUp.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );\n\t\tif( routes.down ) this.controlsDown.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );\n\n\t\t// Prev/next buttons\n\t\tif( routes.left || routes.up ) this.controlsPrev.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );\n\t\tif( routes.right || routes.down ) this.controlsNext.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );\n\n\t\t// Highlight fragment directions\n\t\tlet currentSlide = this.Reveal.getCurrentSlide();\n\t\tif( currentSlide ) {\n\n\t\t\tlet fragmentsRoutes = this.Reveal.fragments.availableRoutes();\n\n\t\t\t// Always apply fragment decorator to prev/next buttons\n\t\t\tif( fragmentsRoutes.prev ) this.controlsPrev.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );\n\t\t\tif( fragmentsRoutes.next ) this.controlsNext.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );\n\n\t\t\t// Apply fragment decorators to directional buttons based on\n\t\t\t// what slide axis they are in\n\t\t\tif( this.Reveal.isVerticalSlide( currentSlide ) ) {\n\t\t\t\tif( fragmentsRoutes.prev ) this.controlsUp.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );\n\t\t\t\tif( fragmentsRoutes.next ) this.controlsDown.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );\n\t\t\t}\n\t\t\telse {\n\t\t\t\tif( fragmentsRoutes.prev ) this.controlsLeft.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );\n\t\t\t\tif( fragmentsRoutes.next ) this.controlsRight.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );\n\t\t\t}\n\n\t\t}\n\n\t\tif( this.Reveal.getConfig().controlsTutorial ) {\n\n\t\t\tlet indices = this.Reveal.getIndices();\n\n\t\t\t// Highlight control arrows with an animation to ensure\n\t\t\t// that the viewer knows how to navigate\n\t\t\tif( !this.Reveal.hasNavigatedVertically() && routes.down ) {\n\t\t\t\tthis.controlsDownArrow.classList.add( 'highlight' );\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthis.controlsDownArrow.classList.remove( 'highlight' );\n\n\t\t\t\tif( this.Reveal.getConfig().rtl ) {\n\n\t\t\t\t\tif( !this.Reveal.hasNavigatedHorizontally() && routes.left && indices.v === 0 ) {\n\t\t\t\t\t\tthis.controlsLeftArrow.classList.add( 'highlight' );\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tthis.controlsLeftArrow.classList.remove( 'highlight' );\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\tif( !this.Reveal.hasNavigatedHorizontally() && routes.right && indices.v === 0 ) {\n\t\t\t\t\t\tthis.controlsRightArrow.classList.add( 'highlight' );\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tthis.controlsRightArrow.classList.remove( 'highlight' );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tdestroy() {\n\n\t\tthis.unbind();\n\t\tthis.element.remove();\n\n\t}\n\n\t/**\n\t * Event handlers for navigation control buttons.\n\t */\n\tonNavigateLeftClicked( event ) {\n\n\t\tevent.preventDefault();\n\t\tthis.Reveal.onUserInput();\n\n\t\tif( this.Reveal.getConfig().navigationMode === 'linear' ) {\n\t\t\tthis.Reveal.prev();\n\t\t}\n\t\telse {\n\t\t\tthis.Reveal.left();\n\t\t}\n\n\t}\n\n\tonNavigateRightClicked( event ) {\n\n\t\tevent.preventDefault();\n\t\tthis.Reveal.onUserInput();\n\n\t\tif( this.Reveal.getConfig().navigationMode === 'linear' ) {\n\t\t\tthis.Reveal.next();\n\t\t}\n\t\telse {\n\t\t\tthis.Reveal.right();\n\t\t}\n\n\t}\n\n\tonNavigateUpClicked( event ) {\n\n\t\tevent.preventDefault();\n\t\tthis.Reveal.onUserInput();\n\n\t\tthis.Reveal.up();\n\n\t}\n\n\tonNavigateDownClicked( event ) {\n\n\t\tevent.preventDefault();\n\t\tthis.Reveal.onUserInput();\n\n\t\tthis.Reveal.down();\n\n\t}\n\n\tonNavigatePrevClicked( event ) {\n\n\t\tevent.preventDefault();\n\t\tthis.Reveal.onUserInput();\n\n\t\tthis.Reveal.prev();\n\n\t}\n\n\tonNavigateNextClicked( event ) {\n\n\t\tevent.preventDefault();\n\t\tthis.Reveal.onUserInput();\n\n\t\tthis.Reveal.next();\n\n\t}\n\n\n}","/**\n * Creates a visual progress bar for the presentation.\n */\nexport default class Progress {\n\n\tconstructor( Reveal ) {\n\n\t\tthis.Reveal = Reveal;\n\n\t\tthis.onProgressClicked = this.onProgressClicked.bind( this );\n\n\t}\n\n\trender() {\n\n\t\tthis.element = document.createElement( 'div' );\n\t\tthis.element.className = 'progress';\n\t\tthis.Reveal.getRevealElement().appendChild( this.element );\n\n\t\tthis.bar = document.createElement( 'span' );\n\t\tthis.element.appendChild( this.bar );\n\n\t}\n\n\t/**\n\t * Called when the reveal.js config is updated.\n\t */\n\tconfigure( config, oldConfig ) {\n\n\t\tthis.element.style.display = config.progress ? 'block' : 'none';\n\n\t}\n\n\tbind() {\n\n\t\tif( this.Reveal.getConfig().progress && this.element ) {\n\t\t\tthis.element.addEventListener( 'click', this.onProgressClicked, false );\n\t\t}\n\n\t}\n\n\tunbind() {\n\n\t\tif ( this.Reveal.getConfig().progress && this.element ) {\n\t\t\tthis.element.removeEventListener( 'click', this.onProgressClicked, false );\n\t\t}\n\n\t}\n\n\t/**\n\t * Updates the progress bar to reflect the current slide.\n\t */\n\tupdate() {\n\n\t\t// Update progress if enabled\n\t\tif( this.Reveal.getConfig().progress && this.bar ) {\n\n\t\t\tlet scale = this.Reveal.getProgress();\n\n\t\t\t// Don't fill the progress bar if there's only one slide\n\t\t\tif( this.Reveal.getTotalSlides() < 2 ) {\n\t\t\t\tscale = 0;\n\t\t\t}\n\n\t\t\tthis.bar.style.transform = 'scaleX('+ scale +')';\n\n\t\t}\n\n\t}\n\n\tgetMaxWidth() {\n\n\t\treturn this.Reveal.getRevealElement().offsetWidth;\n\n\t}\n\n\t/**\n\t * Clicking on the progress bar results in a navigation to the\n\t * closest approximate horizontal slide using this equation:\n\t *\n\t * ( clickX / presentationWidth ) * numberOfSlides\n\t *\n\t * @param {object} event\n\t */\n\tonProgressClicked( event ) {\n\n\t\tthis.Reveal.onUserInput( event );\n\n\t\tevent.preventDefault();\n\n\t\tlet slides = this.Reveal.getSlides();\n\t\tlet slidesTotal = slides.length;\n\t\tlet slideIndex = Math.floor( ( event.clientX / this.getMaxWidth() ) * slidesTotal );\n\n\t\tif( this.Reveal.getConfig().rtl ) {\n\t\t\tslideIndex = slidesTotal - slideIndex;\n\t\t}\n\n\t\tlet targetIndices = this.Reveal.getIndices(slides[slideIndex]);\n\t\tthis.Reveal.slide( targetIndices.h, targetIndices.v );\n\n\t}\n\n\tdestroy() {\n\n\t\tthis.element.remove();\n\n\t}\n\n}","/**\n * Handles hiding of the pointer/cursor when inactive.\n */\nexport default class Pointer {\n\n\tconstructor( Reveal ) {\n\n\t\tthis.Reveal = Reveal;\n\n\t\t// Throttles mouse wheel navigation\n\t\tthis.lastMouseWheelStep = 0;\n\n\t\t// Is the mouse pointer currently hidden from view\n\t\tthis.cursorHidden = false;\n\n\t\t// Timeout used to determine when the cursor is inactive\n\t\tthis.cursorInactiveTimeout = 0;\n\n\t\tthis.onDocumentCursorActive = this.onDocumentCursorActive.bind( this );\n\t\tthis.onDocumentMouseScroll = this.onDocumentMouseScroll.bind( this );\n\n\t}\n\n\t/**\n\t * Called when the reveal.js config is updated.\n\t */\n\tconfigure( config, oldConfig ) {\n\n\t\tif( config.mouseWheel ) {\n\t\t\tdocument.addEventListener( 'DOMMouseScroll', this.onDocumentMouseScroll, false ); // FF\n\t\t\tdocument.addEventListener( 'mousewheel', this.onDocumentMouseScroll, false );\n\t\t}\n\t\telse {\n\t\t\tdocument.removeEventListener( 'DOMMouseScroll', this.onDocumentMouseScroll, false ); // FF\n\t\t\tdocument.removeEventListener( 'mousewheel', this.onDocumentMouseScroll, false );\n\t\t}\n\n\t\t// Auto-hide the mouse pointer when its inactive\n\t\tif( config.hideInactiveCursor ) {\n\t\t\tdocument.addEventListener( 'mousemove', this.onDocumentCursorActive, false );\n\t\t\tdocument.addEventListener( 'mousedown', this.onDocumentCursorActive, false );\n\t\t}\n\t\telse {\n\t\t\tthis.showCursor();\n\n\t\t\tdocument.removeEventListener( 'mousemove', this.onDocumentCursorActive, false );\n\t\t\tdocument.removeEventListener( 'mousedown', this.onDocumentCursorActive, false );\n\t\t}\n\n\t}\n\n\t/**\n\t * Shows the mouse pointer after it has been hidden with\n\t * #hideCursor.\n\t */\n\tshowCursor() {\n\n\t\tif( this.cursorHidden ) {\n\t\t\tthis.cursorHidden = false;\n\t\t\tthis.Reveal.getRevealElement().style.cursor = '';\n\t\t}\n\n\t}\n\n\t/**\n\t * Hides the mouse pointer when it's on top of the .reveal\n\t * container.\n\t */\n\thideCursor() {\n\n\t\tif( this.cursorHidden === false ) {\n\t\t\tthis.cursorHidden = true;\n\t\t\tthis.Reveal.getRevealElement().style.cursor = 'none';\n\t\t}\n\n\t}\n\n\tdestroy() {\n\n\t\tthis.showCursor();\n\n\t\tdocument.removeEventListener( 'DOMMouseScroll', this.onDocumentMouseScroll, false );\n\t\tdocument.removeEventListener( 'mousewheel', this.onDocumentMouseScroll, false );\n\t\tdocument.removeEventListener( 'mousemove', this.onDocumentCursorActive, false );\n\t\tdocument.removeEventListener( 'mousedown', this.onDocumentCursorActive, false );\n\n\t}\n\n\t/**\n\t * Called whenever there is mouse input at the document level\n\t * to determine if the cursor is active or not.\n\t *\n\t * @param {object} event\n\t */\n\tonDocumentCursorActive( event ) {\n\n\t\tthis.showCursor();\n\n\t\tclearTimeout( this.cursorInactiveTimeout );\n\n\t\tthis.cursorInactiveTimeout = setTimeout( this.hideCursor.bind( this ), this.Reveal.getConfig().hideCursorTime );\n\n\t}\n\n\t/**\n\t * Handles mouse wheel scrolling, throttled to avoid skipping\n\t * multiple slides.\n\t *\n\t * @param {object} event\n\t */\n\tonDocumentMouseScroll( event ) {\n\n\t\tif( Date.now() - this.lastMouseWheelStep > 1000 ) {\n\n\t\t\tthis.lastMouseWheelStep = Date.now();\n\n\t\t\tlet delta = event.detail || -event.wheelDelta;\n\t\t\tif( delta > 0 ) {\n\t\t\t\tthis.Reveal.next();\n\t\t\t}\n\t\t\telse if( delta < 0 ) {\n\t\t\t\tthis.Reveal.prev();\n\t\t\t}\n\n\t\t}\n\n\t}\n\n}","/**\n * Loads a JavaScript file from the given URL and executes it.\n *\n * @param {string} url Address of the .js file to load\n * @param {function} callback Method to invoke when the script\n * has loaded and executed\n */\nexport const loadScript = ( url, callback ) => {\n\n\tconst script = document.createElement( 'script' );\n\tscript.type = 'text/javascript';\n\tscript.async = false;\n\tscript.defer = false;\n\tscript.src = url;\n\n\tif( typeof callback === 'function' ) {\n\n\t\t// Success callback\n\t\tscript.onload = script.onreadystatechange = event => {\n\t\t\tif( event.type === 'load' || /loaded|complete/.test( script.readyState ) ) {\n\n\t\t\t\t// Kill event listeners\n\t\t\t\tscript.onload = script.onreadystatechange = script.onerror = null;\n\n\t\t\t\tcallback();\n\n\t\t\t}\n\t\t};\n\n\t\t// Error callback\n\t\tscript.onerror = err => {\n\n\t\t\t// Kill event listeners\n\t\t\tscript.onload = script.onreadystatechange = script.onerror = null;\n\n\t\t\tcallback( new Error( 'Failed loading script: ' + script.src + '\\n' + err ) );\n\n\t\t};\n\n\t}\n\n\t// Append the script at the end of \n\tconst head = document.querySelector( 'head' );\n\thead.insertBefore( script, head.lastChild );\n\n}","import { loadScript } from '../utils/loader.js'\n\n/**\n * Manages loading and registering of reveal.js plugins.\n */\nexport default class Plugins {\n\n\tconstructor( reveal ) {\n\n\t\tthis.Reveal = reveal;\n\n\t\t// Flags our current state (idle -> loading -> loaded)\n\t\tthis.state = 'idle';\n\n\t\t// An id:instance map of currently registed plugins\n\t\tthis.registeredPlugins = {};\n\n\t\tthis.asyncDependencies = [];\n\n\t}\n\n\t/**\n\t * Loads reveal.js dependencies, registers and\n\t * initializes plugins.\n\t *\n\t * Plugins are direct references to a reveal.js plugin\n\t * object that we register and initialize after any\n\t * synchronous dependencies have loaded.\n\t *\n\t * Dependencies are defined via the 'dependencies' config\n\t * option and will be loaded prior to starting reveal.js.\n\t * Some dependencies may have an 'async' flag, if so they\n\t * will load after reveal.js has been started up.\n\t */\n\tload( plugins, dependencies ) {\n\n\t\tthis.state = 'loading';\n\n\t\tplugins.forEach( this.registerPlugin.bind( this ) );\n\n\t\treturn new Promise( resolve => {\n\n\t\t\tlet scripts = [],\n\t\t\t\tscriptsToLoad = 0;\n\n\t\t\tdependencies.forEach( s => {\n\t\t\t\t// Load if there's no condition or the condition is truthy\n\t\t\t\tif( !s.condition || s.condition() ) {\n\t\t\t\t\tif( s.async ) {\n\t\t\t\t\t\tthis.asyncDependencies.push( s );\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tscripts.push( s );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\tif( scripts.length ) {\n\t\t\t\tscriptsToLoad = scripts.length;\n\n\t\t\t\tconst scriptLoadedCallback = (s) => {\n\t\t\t\t\tif( s && typeof s.callback === 'function' ) s.callback();\n\n\t\t\t\t\tif( --scriptsToLoad === 0 ) {\n\t\t\t\t\t\tthis.initPlugins().then( resolve );\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\t// Load synchronous scripts\n\t\t\t\tscripts.forEach( s => {\n\t\t\t\t\tif( typeof s.id === 'string' ) {\n\t\t\t\t\t\tthis.registerPlugin( s );\n\t\t\t\t\t\tscriptLoadedCallback( s );\n\t\t\t\t\t}\n\t\t\t\t\telse if( typeof s.src === 'string' ) {\n\t\t\t\t\t\tloadScript( s.src, () => scriptLoadedCallback(s) );\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tconsole.warn( 'Unrecognized plugin format', s );\n\t\t\t\t\t\tscriptLoadedCallback();\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthis.initPlugins().then( resolve );\n\t\t\t}\n\n\t\t} );\n\n\t}\n\n\t/**\n\t * Initializes our plugins and waits for them to be ready\n\t * before proceeding.\n\t */\n\tinitPlugins() {\n\n\t\treturn new Promise( resolve => {\n\n\t\t\tlet pluginValues = Object.values( this.registeredPlugins );\n\t\t\tlet pluginsToInitialize = pluginValues.length;\n\n\t\t\t// If there are no plugins, skip this step\n\t\t\tif( pluginsToInitialize === 0 ) {\n\t\t\t\tthis.loadAsync().then( resolve );\n\t\t\t}\n\t\t\t// ... otherwise initialize plugins\n\t\t\telse {\n\n\t\t\t\tlet initNextPlugin;\n\n\t\t\t\tlet afterPlugInitialized = () => {\n\t\t\t\t\tif( --pluginsToInitialize === 0 ) {\n\t\t\t\t\t\tthis.loadAsync().then( resolve );\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tinitNextPlugin();\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\tlet i = 0;\n\n\t\t\t\t// Initialize plugins serially\n\t\t\t\tinitNextPlugin = () => {\n\n\t\t\t\t\tlet plugin = pluginValues[i++];\n\n\t\t\t\t\t// If the plugin has an 'init' method, invoke it\n\t\t\t\t\tif( typeof plugin.init === 'function' ) {\n\t\t\t\t\t\tlet promise = plugin.init( this.Reveal );\n\n\t\t\t\t\t\t// If the plugin returned a Promise, wait for it\n\t\t\t\t\t\tif( promise && typeof promise.then === 'function' ) {\n\t\t\t\t\t\t\tpromise.then( afterPlugInitialized );\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tafterPlugInitialized();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tafterPlugInitialized();\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tinitNextPlugin();\n\n\t\t\t}\n\n\t\t} )\n\n\t}\n\n\t/**\n\t * Loads all async reveal.js dependencies.\n\t */\n\tloadAsync() {\n\n\t\tthis.state = 'loaded';\n\n\t\tif( this.asyncDependencies.length ) {\n\t\t\tthis.asyncDependencies.forEach( s => {\n\t\t\t\tloadScript( s.src, s.callback );\n\t\t\t} );\n\t\t}\n\n\t\treturn Promise.resolve();\n\n\t}\n\n\t/**\n\t * Registers a new plugin with this reveal.js instance.\n\t *\n\t * reveal.js waits for all regisered plugins to initialize\n\t * before considering itself ready, as long as the plugin\n\t * is registered before calling `Reveal.initialize()`.\n\t */\n\tregisterPlugin( plugin ) {\n\n\t\t// Backwards compatibility to make reveal.js ~3.9.0\n\t\t// plugins work with reveal.js 4.0.0\n\t\tif( arguments.length === 2 && typeof arguments[0] === 'string' ) {\n\t\t\tplugin = arguments[1];\n\t\t\tplugin.id = arguments[0];\n\t\t}\n\t\t// Plugin can optionally be a function which we call\n\t\t// to create an instance of the plugin\n\t\telse if( typeof plugin === 'function' ) {\n\t\t\tplugin = plugin();\n\t\t}\n\n\t\tlet id = plugin.id;\n\n\t\tif( typeof id !== 'string' ) {\n\t\t\tconsole.warn( 'Unrecognized plugin format; can\\'t find plugin.id', plugin );\n\t\t}\n\t\telse if( this.registeredPlugins[id] === undefined ) {\n\t\t\tthis.registeredPlugins[id] = plugin;\n\n\t\t\t// If a plugin is registered after reveal.js is loaded,\n\t\t\t// initialize it right away\n\t\t\tif( this.state === 'loaded' && typeof plugin.init === 'function' ) {\n\t\t\t\tplugin.init( this.Reveal );\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tconsole.warn( 'reveal.js: \"'+ id +'\" plugin has already been registered' );\n\t\t}\n\n\t}\n\n\t/**\n\t * Checks if a specific plugin has been registered.\n\t *\n\t * @param {String} id Unique plugin identifier\n\t */\n\thasPlugin( id ) {\n\n\t\treturn !!this.registeredPlugins[id];\n\n\t}\n\n\t/**\n\t * Returns the specific plugin instance, if a plugin\n\t * with the given ID has been registered.\n\t *\n\t * @param {String} id Unique plugin identifier\n\t */\n\tgetPlugin( id ) {\n\n\t\treturn this.registeredPlugins[id];\n\n\t}\n\n\tgetRegisteredPlugins() {\n\n\t\treturn this.registeredPlugins;\n\n\t}\n\n\tdestroy() {\n\n\t\tObject.values( this.registeredPlugins ).forEach( plugin => {\n\t\t\tif( typeof plugin.destroy === 'function' ) {\n\t\t\t\tplugin.destroy();\n\t\t\t}\n\t\t} );\n\n\t\tthis.registeredPlugins = {};\n\t\tthis.asyncDependencies = [];\n\n\t}\n\n}\n","import { SLIDES_SELECTOR } from '../utils/constants.js'\nimport { queryAll, createStyleSheet } from '../utils/util.js'\n\n/**\n * Setups up our presentation for printing/exporting to PDF.\n */\nexport default class Print {\n\n\tconstructor( Reveal ) {\n\n\t\tthis.Reveal = Reveal;\n\n\t}\n\n\t/**\n\t * Configures the presentation for printing to a static\n\t * PDF.\n\t */\n\tasync setupPDF() {\n\n\t\tconst config = this.Reveal.getConfig();\n\t\tconst slides = queryAll( this.Reveal.getRevealElement(), SLIDES_SELECTOR )\n\n\t\t// Compute slide numbers now, before we start duplicating slides\n\t\tconst injectPageNumbers = config.slideNumber && /all|print/i.test( config.showSlideNumber );\n\n\t\tconst slideSize = this.Reveal.getComputedSlideSize( window.innerWidth, window.innerHeight );\n\n\t\t// Dimensions of the PDF pages\n\t\tconst pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ),\n\t\t\tpageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) );\n\n\t\t// Dimensions of slides within the pages\n\t\tconst slideWidth = slideSize.width,\n\t\t\tslideHeight = slideSize.height;\n\n\t\tawait new Promise( requestAnimationFrame );\n\n\t\t// Let the browser know what page size we want to print\n\t\tcreateStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0px;}' );\n\n\t\t// Limit the size of certain elements to the dimensions of the slide\n\t\tcreateStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' );\n\n\t\tdocument.documentElement.classList.add( 'print-pdf' );\n\t\tdocument.body.style.width = pageWidth + 'px';\n\t\tdocument.body.style.height = pageHeight + 'px';\n\n\t\tconst viewportElement = document.querySelector( '.reveal-viewport' );\n\t\tlet presentationBackground;\n\t\tif( viewportElement ) {\n\t\t\tconst viewportStyles = window.getComputedStyle( viewportElement );\n\t\t\tif( viewportStyles && viewportStyles.background ) {\n\t\t\t\tpresentationBackground = viewportStyles.background;\n\t\t\t}\n\t\t}\n\n\t\t// Make sure stretch elements fit on slide\n\t\tawait new Promise( requestAnimationFrame );\n\t\tthis.Reveal.layoutSlideContents( slideWidth, slideHeight );\n\n\t\t// Batch scrollHeight access to prevent layout thrashing\n\t\tawait new Promise( requestAnimationFrame );\n\n\t\tconst slideScrollHeights = slides.map( slide => slide.scrollHeight );\n\n\t\tconst pages = [];\n\t\tconst pageContainer = slides[0].parentNode;\n\t\tlet slideNumber = 1;\n\n\t\t// Slide and slide background layout\n\t\tslides.forEach( function( slide, index ) {\n\n\t\t\t// Vertical stacks are not centred since their section\n\t\t\t// children will be\n\t\t\tif( slide.classList.contains( 'stack' ) === false ) {\n\t\t\t\t// Center the slide inside of the page, giving the slide some margin\n\t\t\t\tlet left = ( pageWidth - slideWidth ) / 2;\n\t\t\t\tlet top = ( pageHeight - slideHeight ) / 2;\n\n\t\t\t\tconst contentHeight = slideScrollHeights[ index ];\n\t\t\t\tlet numberOfPages = Math.max( Math.ceil( contentHeight / pageHeight ), 1 );\n\n\t\t\t\t// Adhere to configured pages per slide limit\n\t\t\t\tnumberOfPages = Math.min( numberOfPages, config.pdfMaxPagesPerSlide );\n\n\t\t\t\t// Center slides vertically\n\t\t\t\tif( numberOfPages === 1 && config.center || slide.classList.contains( 'center' ) ) {\n\t\t\t\t\ttop = Math.max( ( pageHeight - contentHeight ) / 2, 0 );\n\t\t\t\t}\n\n\t\t\t\t// Wrap the slide in a page element and hide its overflow\n\t\t\t\t// so that no page ever flows onto another\n\t\t\t\tconst page = document.createElement( 'div' );\n\t\t\t\tpages.push( page );\n\n\t\t\t\tpage.className = 'pdf-page';\n\t\t\t\tpage.style.height = ( ( pageHeight + config.pdfPageHeightOffset ) * numberOfPages ) + 'px';\n\n\t\t\t\t// Copy the presentation-wide background to each individual\n\t\t\t\t// page when printing\n\t\t\t\tif( presentationBackground ) {\n\t\t\t\t\tpage.style.background = presentationBackground;\n\t\t\t\t}\n\n\t\t\t\tpage.appendChild( slide );\n\n\t\t\t\t// Position the slide inside of the page\n\t\t\t\tslide.style.left = left + 'px';\n\t\t\t\tslide.style.top = top + 'px';\n\t\t\t\tslide.style.width = slideWidth + 'px';\n\n\t\t\t\tthis.Reveal.slideContent.layout( slide );\n\n\t\t\t\tif( slide.slideBackgroundElement ) {\n\t\t\t\t\tpage.insertBefore( slide.slideBackgroundElement, slide );\n\t\t\t\t}\n\n\t\t\t\t// Inject notes if `showNotes` is enabled\n\t\t\t\tif( config.showNotes ) {\n\n\t\t\t\t\t// Are there notes for this slide?\n\t\t\t\t\tconst notes = this.Reveal.getSlideNotes( slide );\n\t\t\t\t\tif( notes ) {\n\n\t\t\t\t\t\tconst notesSpacing = 8;\n\t\t\t\t\t\tconst notesLayout = typeof config.showNotes === 'string' ? config.showNotes : 'inline';\n\t\t\t\t\t\tconst notesElement = document.createElement( 'div' );\n\t\t\t\t\t\tnotesElement.classList.add( 'speaker-notes' );\n\t\t\t\t\t\tnotesElement.classList.add( 'speaker-notes-pdf' );\n\t\t\t\t\t\tnotesElement.setAttribute( 'data-layout', notesLayout );\n\t\t\t\t\t\tnotesElement.innerHTML = notes;\n\n\t\t\t\t\t\tif( notesLayout === 'separate-page' ) {\n\t\t\t\t\t\t\tpages.push( notesElement );\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tnotesElement.style.left = notesSpacing + 'px';\n\t\t\t\t\t\t\tnotesElement.style.bottom = notesSpacing + 'px';\n\t\t\t\t\t\t\tnotesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px';\n\t\t\t\t\t\t\tpage.appendChild( notesElement );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\t// Inject page numbers if `slideNumbers` are enabled\n\t\t\t\tif( injectPageNumbers ) {\n\t\t\t\t\tconst numberElement = document.createElement( 'div' );\n\t\t\t\t\tnumberElement.classList.add( 'slide-number' );\n\t\t\t\t\tnumberElement.classList.add( 'slide-number-pdf' );\n\t\t\t\t\tnumberElement.innerHTML = slideNumber++;\n\t\t\t\t\tpage.appendChild( numberElement );\n\t\t\t\t}\n\n\t\t\t\t// Copy page and show fragments one after another\n\t\t\t\tif( config.pdfSeparateFragments ) {\n\n\t\t\t\t\t// Each fragment 'group' is an array containing one or more\n\t\t\t\t\t// fragments. Multiple fragments that appear at the same time\n\t\t\t\t\t// are part of the same group.\n\t\t\t\t\tconst fragmentGroups = this.Reveal.fragments.sort( page.querySelectorAll( '.fragment' ), true );\n\n\t\t\t\t\tlet previousFragmentStep;\n\n\t\t\t\t\tfragmentGroups.forEach( function( fragments, index ) {\n\n\t\t\t\t\t\t// Remove 'current-fragment' from the previous group\n\t\t\t\t\t\tif( previousFragmentStep ) {\n\t\t\t\t\t\t\tpreviousFragmentStep.forEach( function( fragment ) {\n\t\t\t\t\t\t\t\tfragment.classList.remove( 'current-fragment' );\n\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Show the fragments for the current index\n\t\t\t\t\t\tfragments.forEach( function( fragment ) {\n\t\t\t\t\t\t\tfragment.classList.add( 'visible', 'current-fragment' );\n\t\t\t\t\t\t}, this );\n\n\t\t\t\t\t\t// Create a separate page for the current fragment state\n\t\t\t\t\t\tconst clonedPage = page.cloneNode( true );\n\n\t\t\t\t\t\t// Inject unique page numbers for fragments\n\t\t\t\t\t\tif( injectPageNumbers ) {\n\t\t\t\t\t\t\tconst numberElement = clonedPage.querySelector( '.slide-number-pdf' );\n\t\t\t\t\t\t\tconst fragmentNumber = index + 1;\n\t\t\t\t\t\t\tnumberElement.innerHTML += '.' + fragmentNumber;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tpages.push( clonedPage );\n\n\t\t\t\t\t\tpreviousFragmentStep = fragments;\n\n\t\t\t\t\t}, this );\n\n\t\t\t\t\t// Reset the first/original page so that all fragments are hidden\n\t\t\t\t\tfragmentGroups.forEach( function( fragments ) {\n\t\t\t\t\t\tfragments.forEach( function( fragment ) {\n\t\t\t\t\t\t\tfragment.classList.remove( 'visible', 'current-fragment' );\n\t\t\t\t\t\t} );\n\t\t\t\t\t} );\n\n\t\t\t\t}\n\t\t\t\t// Show all fragments\n\t\t\t\telse {\n\t\t\t\t\tqueryAll( page, '.fragment:not(.fade-out)' ).forEach( function( fragment ) {\n\t\t\t\t\t\tfragment.classList.add( 'visible' );\n\t\t\t\t\t} );\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}, this );\n\n\t\tawait new Promise( requestAnimationFrame );\n\n\t\tpages.forEach( page => pageContainer.appendChild( page ) );\n\n\t\t// Re-run JS-based content layout after the slide is added to page DOM\n\t\tthis.Reveal.slideContent.layout( this.Reveal.getSlidesElement() );\n\n\t\t// Notify subscribers that the PDF layout is good to go\n\t\tthis.Reveal.dispatchEvent({ type: 'pdf-ready' });\n\n\t}\n\n\t/**\n\t * Checks if this instance is being used to print a PDF.\n\t */\n\tisPrintingPDF() {\n\n\t\treturn ( /print-pdf/gi ).test( window.location.search );\n\n\t}\n\n}\n","import { isAndroid } from '../utils/device.js'\nimport { matches } from '../utils/util.js'\n\nconst SWIPE_THRESHOLD = 40;\n\n/**\n * Controls all touch interactions and navigations for\n * a presentation.\n */\nexport default class Touch {\n\n\tconstructor( Reveal ) {\n\n\t\tthis.Reveal = Reveal;\n\n\t\t// Holds information about the currently ongoing touch interaction\n\t\tthis.touchStartX = 0;\n\t\tthis.touchStartY = 0;\n\t\tthis.touchStartCount = 0;\n\t\tthis.touchCaptured = false;\n\n\t\tthis.onPointerDown = this.onPointerDown.bind( this );\n\t\tthis.onPointerMove = this.onPointerMove.bind( this );\n\t\tthis.onPointerUp = this.onPointerUp.bind( this );\n\t\tthis.onTouchStart = this.onTouchStart.bind( this );\n\t\tthis.onTouchMove = this.onTouchMove.bind( this );\n\t\tthis.onTouchEnd = this.onTouchEnd.bind( this );\n\n\t}\n\n\t/**\n\t *\n\t */\n\tbind() {\n\n\t\tlet revealElement = this.Reveal.getRevealElement();\n\n\t\tif( 'onpointerdown' in window ) {\n\t\t\t// Use W3C pointer events\n\t\t\trevealElement.addEventListener( 'pointerdown', this.onPointerDown, false );\n\t\t\trevealElement.addEventListener( 'pointermove', this.onPointerMove, false );\n\t\t\trevealElement.addEventListener( 'pointerup', this.onPointerUp, false );\n\t\t}\n\t\telse if( window.navigator.msPointerEnabled ) {\n\t\t\t// IE 10 uses prefixed version of pointer events\n\t\t\trevealElement.addEventListener( 'MSPointerDown', this.onPointerDown, false );\n\t\t\trevealElement.addEventListener( 'MSPointerMove', this.onPointerMove, false );\n\t\t\trevealElement.addEventListener( 'MSPointerUp', this.onPointerUp, false );\n\t\t}\n\t\telse {\n\t\t\t// Fall back to touch events\n\t\t\trevealElement.addEventListener( 'touchstart', this.onTouchStart, false );\n\t\t\trevealElement.addEventListener( 'touchmove', this.onTouchMove, false );\n\t\t\trevealElement.addEventListener( 'touchend', this.onTouchEnd, false );\n\t\t}\n\n\t}\n\n\t/**\n\t *\n\t */\n\tunbind() {\n\n\t\tlet revealElement = this.Reveal.getRevealElement();\n\n\t\trevealElement.removeEventListener( 'pointerdown', this.onPointerDown, false );\n\t\trevealElement.removeEventListener( 'pointermove', this.onPointerMove, false );\n\t\trevealElement.removeEventListener( 'pointerup', this.onPointerUp, false );\n\n\t\trevealElement.removeEventListener( 'MSPointerDown', this.onPointerDown, false );\n\t\trevealElement.removeEventListener( 'MSPointerMove', this.onPointerMove, false );\n\t\trevealElement.removeEventListener( 'MSPointerUp', this.onPointerUp, false );\n\n\t\trevealElement.removeEventListener( 'touchstart', this.onTouchStart, false );\n\t\trevealElement.removeEventListener( 'touchmove', this.onTouchMove, false );\n\t\trevealElement.removeEventListener( 'touchend', this.onTouchEnd, false );\n\n\t}\n\n\t/**\n\t * Checks if the target element prevents the triggering of\n\t * swipe navigation.\n\t */\n\tisSwipePrevented( target ) {\n\n\t\t// Prevent accidental swipes when scrubbing timelines\n\t\tif( matches( target, 'video, audio' ) ) return true;\n\n\t\twhile( target && typeof target.hasAttribute === 'function' ) {\n\t\t\tif( target.hasAttribute( 'data-prevent-swipe' ) ) return true;\n\t\t\ttarget = target.parentNode;\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Handler for the 'touchstart' event, enables support for\n\t * swipe and pinch gestures.\n\t *\n\t * @param {object} event\n\t */\n\tonTouchStart( event ) {\n\n\t\tif( this.isSwipePrevented( event.target ) ) return true;\n\n\t\tthis.touchStartX = event.touches[0].clientX;\n\t\tthis.touchStartY = event.touches[0].clientY;\n\t\tthis.touchStartCount = event.touches.length;\n\n\t}\n\n\t/**\n\t * Handler for the 'touchmove' event.\n\t *\n\t * @param {object} event\n\t */\n\tonTouchMove( event ) {\n\n\t\tif( this.isSwipePrevented( event.target ) ) return true;\n\n\t\tlet config = this.Reveal.getConfig();\n\n\t\t// Each touch should only trigger one action\n\t\tif( !this.touchCaptured ) {\n\t\t\tthis.Reveal.onUserInput( event );\n\n\t\t\tlet currentX = event.touches[0].clientX;\n\t\t\tlet currentY = event.touches[0].clientY;\n\n\t\t\t// There was only one touch point, look for a swipe\n\t\t\tif( event.touches.length === 1 && this.touchStartCount !== 2 ) {\n\n\t\t\t\tlet availableRoutes = this.Reveal.availableRoutes({ includeFragments: true });\n\n\t\t\t\tlet deltaX = currentX - this.touchStartX,\n\t\t\t\t\tdeltaY = currentY - this.touchStartY;\n\n\t\t\t\tif( deltaX > SWIPE_THRESHOLD && Math.abs( deltaX ) > Math.abs( deltaY ) ) {\n\t\t\t\t\tthis.touchCaptured = true;\n\t\t\t\t\tif( config.navigationMode === 'linear' ) {\n\t\t\t\t\t\tif( config.rtl ) {\n\t\t\t\t\t\t\tthis.Reveal.next();\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tthis.Reveal.prev();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tthis.Reveal.left();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if( deltaX < -SWIPE_THRESHOLD && Math.abs( deltaX ) > Math.abs( deltaY ) ) {\n\t\t\t\t\tthis.touchCaptured = true;\n\t\t\t\t\tif( config.navigationMode === 'linear' ) {\n\t\t\t\t\t\tif( config.rtl ) {\n\t\t\t\t\t\t\tthis.Reveal.prev();\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tthis.Reveal.next();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tthis.Reveal.right();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if( deltaY > SWIPE_THRESHOLD && availableRoutes.up ) {\n\t\t\t\t\tthis.touchCaptured = true;\n\t\t\t\t\tif( config.navigationMode === 'linear' ) {\n\t\t\t\t\t\tthis.Reveal.prev();\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tthis.Reveal.up();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if( deltaY < -SWIPE_THRESHOLD && availableRoutes.down ) {\n\t\t\t\t\tthis.touchCaptured = true;\n\t\t\t\t\tif( config.navigationMode === 'linear' ) {\n\t\t\t\t\t\tthis.Reveal.next();\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tthis.Reveal.down();\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// If we're embedded, only block touch events if they have\n\t\t\t\t// triggered an action\n\t\t\t\tif( config.embedded ) {\n\t\t\t\t\tif( this.touchCaptured || this.Reveal.isVerticalSlide() ) {\n\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Not embedded? Block them all to avoid needless tossing\n\t\t\t\t// around of the viewport in iOS\n\t\t\t\telse {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t\t// There's a bug with swiping on some Android devices unless\n\t\t// the default action is always prevented\n\t\telse if( isAndroid ) {\n\t\t\tevent.preventDefault();\n\t\t}\n\n\t}\n\n\t/**\n\t * Handler for the 'touchend' event.\n\t *\n\t * @param {object} event\n\t */\n\tonTouchEnd( event ) {\n\n\t\tthis.touchCaptured = false;\n\n\t}\n\n\t/**\n\t * Convert pointer down to touch start.\n\t *\n\t * @param {object} event\n\t */\n\tonPointerDown( event ) {\n\n\t\tif( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === \"touch\" ) {\n\t\t\tevent.touches = [{ clientX: event.clientX, clientY: event.clientY }];\n\t\t\tthis.onTouchStart( event );\n\t\t}\n\n\t}\n\n\t/**\n\t * Convert pointer move to touch move.\n\t *\n\t * @param {object} event\n\t */\n\tonPointerMove( event ) {\n\n\t\tif( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === \"touch\" ) {\n\t\t\tevent.touches = [{ clientX: event.clientX, clientY: event.clientY }];\n\t\t\tthis.onTouchMove( event );\n\t\t}\n\n\t}\n\n\t/**\n\t * Convert pointer up to touch end.\n\t *\n\t * @param {object} event\n\t */\n\tonPointerUp( event ) {\n\n\t\tif( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === \"touch\" ) {\n\t\t\tevent.touches = [{ clientX: event.clientX, clientY: event.clientY }];\n\t\t\tthis.onTouchEnd( event );\n\t\t}\n\n\t}\n\n}","import { closest } from '../utils/util.js'\n\n/**\n * Manages focus when a presentation is embedded. This\n * helps us only capture keyboard from the presentation\n * a user is currently interacting with in a page where\n * multiple presentations are embedded.\n */\n\nconst STATE_FOCUS = 'focus';\nconst STATE_BLUR = 'blur';\n\nexport default class Focus {\n\n\tconstructor( Reveal ) {\n\n\t\tthis.Reveal = Reveal;\n\n\t\tthis.onRevealPointerDown = this.onRevealPointerDown.bind( this );\n\t\tthis.onDocumentPointerDown = this.onDocumentPointerDown.bind( this );\n\n\t}\n\n\t/**\n\t * Called when the reveal.js config is updated.\n\t */\n\tconfigure( config, oldConfig ) {\n\n\t\tif( config.embedded ) {\n\t\t\tthis.blur();\n\t\t}\n\t\telse {\n\t\t\tthis.focus();\n\t\t\tthis.unbind();\n\t\t}\n\n\t}\n\n\tbind() {\n\n\t\tif( this.Reveal.getConfig().embedded ) {\n\t\t\tthis.Reveal.getRevealElement().addEventListener( 'pointerdown', this.onRevealPointerDown, false );\n\t\t}\n\n\t}\n\n\tunbind() {\n\n\t\tthis.Reveal.getRevealElement().removeEventListener( 'pointerdown', this.onRevealPointerDown, false );\n\t\tdocument.removeEventListener( 'pointerdown', this.onDocumentPointerDown, false );\n\n\t}\n\n\tfocus() {\n\n\t\tif( this.state !== STATE_FOCUS ) {\n\t\t\tthis.Reveal.getRevealElement().classList.add( 'focused' );\n\t\t\tdocument.addEventListener( 'pointerdown', this.onDocumentPointerDown, false );\n\t\t}\n\n\t\tthis.state = STATE_FOCUS;\n\n\t}\n\n\tblur() {\n\n\t\tif( this.state !== STATE_BLUR ) {\n\t\t\tthis.Reveal.getRevealElement().classList.remove( 'focused' );\n\t\t\tdocument.removeEventListener( 'pointerdown', this.onDocumentPointerDown, false );\n\t\t}\n\n\t\tthis.state = STATE_BLUR;\n\n\t}\n\n\tisFocused() {\n\n\t\treturn this.state === STATE_FOCUS;\n\n\t}\n\n\tdestroy() {\n\n\t\tthis.Reveal.getRevealElement().classList.remove( 'focused' );\n\n\t}\n\n\tonRevealPointerDown( event ) {\n\n\t\tthis.focus();\n\n\t}\n\n\tonDocumentPointerDown( event ) {\n\n\t\tlet revealElement = closest( event.target, '.reveal' );\n\t\tif( !revealElement || revealElement !== this.Reveal.getRevealElement() ) {\n\t\t\tthis.blur();\n\t\t}\n\n\t}\n\n}","/**\n * Handles the showing and \n */\nexport default class Notes {\n\n\tconstructor( Reveal ) {\n\n\t\tthis.Reveal = Reveal;\n\n\t}\n\n\trender() {\n\n\t\tthis.element = document.createElement( 'div' );\n\t\tthis.element.className = 'speaker-notes';\n\t\tthis.element.setAttribute( 'data-prevent-swipe', '' );\n\t\tthis.element.setAttribute( 'tabindex', '0' );\n\t\tthis.Reveal.getRevealElement().appendChild( this.element );\n\n\t}\n\n\t/**\n\t * Called when the reveal.js config is updated.\n\t */\n\tconfigure( config, oldConfig ) {\n\n\t\tif( config.showNotes ) {\n\t\t\tthis.element.setAttribute( 'data-layout', typeof config.showNotes === 'string' ? config.showNotes : 'inline' );\n\t\t}\n\n\t}\n\n\t/**\n\t * Pick up notes from the current slide and display them\n\t * to the viewer.\n\t *\n\t * @see {@link config.showNotes}\n\t */\n\tupdate() {\n\n\t\tif( this.Reveal.getConfig().showNotes && this.element && this.Reveal.getCurrentSlide() && !this.Reveal.print.isPrintingPDF() ) {\n\n\t\t\tthis.element.innerHTML = this.getSlideNotes() || 'No notes on this slide.';\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Updates the visibility of the speaker notes sidebar that\n\t * is used to share annotated slides. The notes sidebar is\n\t * only visible if showNotes is true and there are notes on\n\t * one or more slides in the deck.\n\t */\n\tupdateVisibility() {\n\n\t\tif( this.Reveal.getConfig().showNotes && this.hasNotes() && !this.Reveal.print.isPrintingPDF() ) {\n\t\t\tthis.Reveal.getRevealElement().classList.add( 'show-notes' );\n\t\t}\n\t\telse {\n\t\t\tthis.Reveal.getRevealElement().classList.remove( 'show-notes' );\n\t\t}\n\n\t}\n\n\t/**\n\t * Checks if there are speaker notes for ANY slide in the\n\t * presentation.\n\t */\n\thasNotes() {\n\n\t\treturn this.Reveal.getSlidesElement().querySelectorAll( '[data-notes], aside.notes' ).length > 0;\n\n\t}\n\n\t/**\n\t * Checks if this presentation is running inside of the\n\t * speaker notes window.\n\t *\n\t * @return {boolean}\n\t */\n\tisSpeakerNotesWindow() {\n\n\t\treturn !!window.location.search.match( /receiver/gi );\n\n\t}\n\n\t/**\n\t * Retrieves the speaker notes from a slide. Notes can be\n\t * defined in two ways:\n\t * 1. As a data-notes attribute on the slide
\n\t * 2. As an
+
+
+

Message-Streams: Reactive Streams API

+

set of interactions with composable message streams

+

🌊flow control ⚠️error handling 🚫cancellation

+
+
+
+

Message-Streams

+

Smallrye-mutiny from Quarkus / RedHat

+
+ +
+
+

Message-Streams

+

Helidon-commons-reactive from Helidon / Oracle

+
+ +
+
+

Message-Streams

+

Project-reactor from SpringBoot / Vmware

+
+ +
+
+

Message-Streams

+

Rxjava3 from ReactiveX

+
+ +
+
+

Message-Streams: message

+

binary data with optional metadata

+
+
+
+
+

Message-Streams: FUTURES API

+

subset of non-streaming interactions

+

CompletableFuture from Jdk8 ☕

+
+
+ +
+
+

Message-Streams: RPC

+
    +
  • 🇬 Protobuf/codegen RPC system on top of Message-Streams
  • +
  • 🇬 GRPC compatibility
  • +
  • 🇬 GRPC-like devexp: schema, compiler, client/server stubs
  • +
  • 🔧request ranks, idempotent requests
  • +
  • 🔧 non-opinionated instrumentation: prometheus, micrometer OOTB
  • +
  • 🌊 completablefuture, smallrye-mutiny, helidon, project-reactor, rxjava3
  • +
+
+
+

Message-Streams: RPC schema

+

Protobuf service 📄API definition

+
+
+
+

Message-Streams: RPC codegen stubs

+

RPC-compiler generated service 📄API

+
+
+
+

Message-Streams: RPC codegen stubs

+

RPC-compiler generated client & server stubs

+
+
+
+
+

Message-Streams: RPC

+

Interop demo. Kitchen application services

+ + https://github.com/jauntsdn/rsocket-jvm-interop-examples + +
    +
  • 🌾Farmer: Mstreams-RPC-reactor / TCP
  • +
  • 📄Recipes: Mstreams-RPC-futures / TCP
  • +
  • 🧑‍🍳Roundsman: Mstreams-RPC-mutiny / ws-http2
  • +
  • 👨‍🍳Chef: Mstreams-RPC-helidon / unix sockets
  • +
  • 🔪Kitchen: Mstreams-RPC-rxjava / GRPC
  • +
  • 🍽️Gourmet: GRPC/grpc-java client
  • +
+
+ +
+

Message-Streams: runtime

+ + + +

extended alternative: JAUNTSDN / RSocket-JVM

+ + + +
+
+
+

Message-Streams: runtime

+

Jauntsdn / RSocket-JVM

+

⚡ alternative to RSocket/RSocket-java

+

of reactive.foundation [🪦 fall 2021]

+

MOTIVATIONS

+
    +
  • ⛔ bad perf: convoluted internals, dropped ball on RPC, use reflection for data[1]
  • +
  • ⛔ vulnerabilities: 4 trivial denial-of-service cases[2]
  • +
  • ⛔ no reusable parts for other vendor API, 🍃Spring libs only: reactor/reactor-netty, micrometer +
  • +
+
+
+

RSocket / RSocket-java

+

Vulnerabilities

+

by reactive.foundation [🪦 fall 2021]

+
    +
  • ⛔ D-O-S: FD exhaustion, memory overflow [2]
  • +
  • ⛔ 3 of 4 interactions: request-response, request-stream, request-channel
  • +
  • ⛔ last release: early Jun 2021
  • +
+

+

🍃 team as main 📣, adopter & maintainer: 🙈🙉🙊 +

+
+
+
+

Message-Streams: runtime

+

RSocket protocol

+
    +
  • + ⚡low latency/high throughput L5 protocol + intended for high-performance services communication +
  • +
  • + symmetric interactions, 2 peers may start requests +
  • +
  • + 🌐 transport agnostic, runs on top of any reliable byte stream transport +
  • +
  • + 🌊 superset of Message-Streams +
  • +
+
+
+
+

Message-Streams: API, RPC

+

🌊flow control ⚠️error handling 🚫cancellation

+
+
+
+

RSocket-JVM: runtime

+

🏃 runtime: networking, transports - datacenter & internet

+

☁️ features: load estimation/circuit breaking, load balancing, real graceful close

+
+
+
+
+

Message-Streams: runtime

+

Jauntsdn / RSocket-JVM

+
    +
  • + ⚡high perf, tiny garbage production, + may serve 1 million streams with commodity PC +
  • +
  • + 🌐 transports: TCP, unix sockets, websockets-over-http2, 🇬 GRPC compatibility +
  • +
  • + ☁️ requests leasing: circuit breaking, + adaptive latency-based load estimation for lightweight load balancing +
  • +
  • + ☁️ request ranks: keep critical services under load, driven by responder with request leasing +
  • +
+
+
+
    +
  • + ☁️ scalable keep-alives, lightweight coarse scheduler for timeouts +
  • +
  • + ☁️ graceful shutdown: client & server +
  • +
  • + ☁️ non-opinionated metrics: prometheus, micrometer OOTB +
  • +
  • + ☁️ symmetric interactions +
  • +
+
+
+
+
+

Message-Streams: runtime

+

Jauntsdn / RSocket-JVM core

+

♻️ Protocol core is shared by vendor libs: mutiny, reactor etc

+
    +
  • + ☮️♻️ components: framing, handshakes, keep-alives, requests leasing, + graceful close, error handling etc +
  • +
  • + ☮️ tiny & neutral: netty-buffer is only dependency +
  • +
  • + ☮️♻️ modules: transports, load estimators, metrics +
  • +
+
+
+
+

Message-Streams: runtime

+

request leasing

+

⏳🎫Responder: time limited request credit: allow (duration, requests, rank)

+
    +
  • Circuit breaking: responder driven / requester aware
  • +
  • Responder: request stats
  • +
  • Credit = f(svc latency stats/ success rate + RTT)
  • +
  • Duration (interval): cap requests rate
  • +
  • Lease = credit + request rank: service brownout
  • +
  • Requester: RSocket.availability(rank) [0 ; 1]
  • +
+
+
+

Message-Streams: runtime

+

leasing: stats

+
+
+
+
+

Message-Streams: runtime

+

leasing: load balancing

+
    +
  • 📖theory: min(set(RSocket.availability)) + conn RTT
  • +
  • 🔨practice: more complex - still cpu-efficient
  • +
  • LB + lease: CPU utilization + connection RTT
  • +
  • LB + lease metadata: allowed fnf, indicative service latencies
  • +
  • service latency: call hash + latency
  • +
+
+
+

Message-Streams: runtime

+

RPC origin lease: latency based load estimator

+
+
+
+

Message-Streams: runtime

+

RPC origin lease: latency based load estimator

+
    +
  • AIMD: additive inc⬆️ / multiplicative dec⬇️
  • +
  • service ⬅️response latency
  • +
  • 🔄RTT latency
  • +
  • ⚠️error rate
  • +
  • ♨️CPU / memory utilization
  • +
  • ♨️leased capacity utilization
  • +
+
+
+
+

Message-Streams: RPC

+

Interop demo. Kitchen application services

+ + https://github.com/jauntsdn/rsocket-jvm-interop-examples + +
    +
  • 🌾Farmer: Mstreams-RPC-reactor / TCP
  • +
  • 📄Recipes: Mstreams-RPC-futures / TCP
  • +
  • 🧑‍🍳Roundsman: Mstreams-RPC-mutiny / ws-http2
  • +
  • 👨‍🍳Chef: Mstreams-RPC-helidon / unix sockets
  • +
  • 🔪Kitchen: Mstreams-RPC-rxjava / GRPC
  • +
  • 🍽️Gourmet: GRPC/grpc-java client
  • +
+
+
+
+

Message-Streams: runtime

+

RPC througput test

+

⚡Single vCPU RYZEN™ 5 2600X, reactor, non-TLS, TCP EPOLL /w linux 5.4.0, jdk11

+ + + + + + + + + + + + + + + + + + + + + + + + + +
msg size, bytes8128512
request-response1.45mil1.0mil0.55mil
request-stream3.3mil2.4mil0.9mil
request-channel3.5mil2.4mil1.25mil
+
+
+

Message-Streams: runtime

+

GRPC-RPC througput test

+

🦥GRPC-java client, MStreams RPC server. Single vCPU RYZEN™ 5 2600X, reactor, non-TLS, TCP EPOLL /w linux 5.4.0, jdk11

+ + + + + + + + + + + + + + + + + +
msg size, bytes8
request-response0.1mil
request-stream1.4mil
request-channel1.2mil
+
+
+
+
+

Message-Streams: runtime

+

Load test

+

♨️♨️1 million streams, 10k conns, single host [3]

+
    +
  • Host: 16 vCPU / 32gb RAM, linux 5.4.0, jdk11
  • +
  • Scheduler: Nomad/Consul
  • +
  • 1 server: 8vCPU/16gb, 2 clients: 4vCPU/8gb each
  • +
  • TCP non-TLS /w EPOLL IO
  • +
+
+
+

Server, simultaneous streams

+

♨️1 million streams over 10k connections, monotonically over ~16 mins

+ +
+
+

Host CPU usage

+

♨️server: 60% of host CPU, 85% overall used by load test

+ +
+
+

Server, memory alloc rate

+

❄️Peaks at only 50 Mb/sec => 50 bytes per inbound message

+ +
+
+
+
+

Message-Streams: RPC

+

Size & start time

+

☕CompletableFutures: 1st request time & binaries size

+ + + + + + + + + + + + + + + + + + + + + +
appTCP/-TLSwebsocket-http2/+TLS
svc size2.9MB2.9MB
app size5.3MB7.7MB
1st request ⏳350 ms600 ms
+
    +
  • svc: netty 0.9MB, protobuf 1.7MB - room for 🔧
  • +
+
+
+

Message-Streams: RPC

+

Distribution size

+

☕CompletableFutures: application image (/w JRE17) & alpine docker image size

+ + + + + + + + + + + + + + + + +
appTCPwebsocket-http2/TLS
App JRE image49.5MB51.9MB
App alpine 📦57MB59.4MB
+
+
+

Message-Streams: RPC

+

Native binary size

+

☕CompletableFutures: native executable /w Graalvm11, serial GC

+
    +
  • ⚡TCP native binary size: 20.5MB, startup: instant
  • +
+
+
+ +
+

Summary

+
    +
  • ⚡lean & very fast (millions messages/core) GRPC-like services on JVM with rich streaming model
  • +
  • multiple APIs: ☕CompletableFuture; 🌊streaming with reactor, rxjava, mutiny, helidon
  • +
  • pluggable networking: ⚡tcp, unix sockets; 🌐grpc, websockets-over-http2
  • +
  • ☁️service APIs/codegen stubs [Message-Streams] are split from library runtime [RSocket-JVM]
  • +
  • ☁️transparent origin [RPC] & proxy load estimation which enables cpu-efficient load balancers
  • +
  • ☁️native image support with graalvm
  • +
+
+
+ + + +

Questions

+ jauntsdn.com / mostroverkhov@gmail / ostroverkhov@twitter / mostroverkhov@github +
+
+

References

+

+ + + [1] https://jauntsdn.com/post/rsocket-million-streams-2/ + +

+

+ + + [2] https://github.com/spring-projects/spring-framework/issues/27373 + + +

+

+ + +      https://github.com/spring-projects/spring-framework/issues/27427 + + +

+

+ + +      https://github.com/spring-projects/spring-framework/issues/27428 + + +

+

+ + +      https://github.com/spring-projects/spring-framework/issues/27462 + + +

+

+ + + [3] https://jauntsdn.com/post/rsocket-million-streams-1/ + + +

+
+
+ +
+ + + + + + + + + + + diff --git a/mstreams/presentation/message-streams/1000k-alloc-part2.png b/mstreams/presentation/message-streams/1000k-alloc-part2.png new file mode 100644 index 0000000..a8ac3d4 Binary files /dev/null and b/mstreams/presentation/message-streams/1000k-alloc-part2.png differ diff --git a/mstreams/presentation/message-streams/1000k-cpu-part2.png b/mstreams/presentation/message-streams/1000k-cpu-part2.png new file mode 100644 index 0000000..e927a7d Binary files /dev/null and b/mstreams/presentation/message-streams/1000k-cpu-part2.png differ diff --git a/mstreams/presentation/message-streams/1000k-streams-part2.png b/mstreams/presentation/message-streams/1000k-streams-part2.png new file mode 100644 index 0000000..dff644c Binary files /dev/null and b/mstreams/presentation/message-streams/1000k-streams-part2.png differ diff --git a/mstreams/presentation/message-streams/helidon-logo.png b/mstreams/presentation/message-streams/helidon-logo.png new file mode 100644 index 0000000..bf9337f Binary files /dev/null and b/mstreams/presentation/message-streams/helidon-logo.png differ diff --git a/mstreams/presentation/message-streams/jaunt-logo-text.png b/mstreams/presentation/message-streams/jaunt-logo-text.png new file mode 100644 index 0000000..81ffe39 Binary files /dev/null and b/mstreams/presentation/message-streams/jaunt-logo-text.png differ diff --git a/mstreams/presentation/message-streams/mstreams.png b/mstreams/presentation/message-streams/mstreams.png new file mode 100644 index 0000000..8da51e6 Binary files /dev/null and b/mstreams/presentation/message-streams/mstreams.png differ diff --git a/mstreams/presentation/message-streams/reactor-logo.png b/mstreams/presentation/message-streams/reactor-logo.png new file mode 100644 index 0000000..c59319b Binary files /dev/null and b/mstreams/presentation/message-streams/reactor-logo.png differ diff --git a/mstreams/presentation/message-streams/rsocket-logo.svg b/mstreams/presentation/message-streams/rsocket-logo.svg new file mode 100644 index 0000000..343c1a0 --- /dev/null +++ b/mstreams/presentation/message-streams/rsocket-logo.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + diff --git a/mstreams/presentation/message-streams/rxjava-logo.png b/mstreams/presentation/message-streams/rxjava-logo.png new file mode 100644 index 0000000..948871d Binary files /dev/null and b/mstreams/presentation/message-streams/rxjava-logo.png differ diff --git a/mstreams/presentation/message-streams/smallrye-logo.svg b/mstreams/presentation/message-streams/smallrye-logo.svg new file mode 100644 index 0000000..64d964f --- /dev/null +++ b/mstreams/presentation/message-streams/smallrye-logo.svg @@ -0,0 +1,28 @@ + + + + + smallrye_assets + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mstreams/presentation/plugin/highlight/highlight.esm.js b/mstreams/presentation/plugin/highlight/highlight.esm.js new file mode 100644 index 0000000..20f35d7 --- /dev/null +++ b/mstreams/presentation/plugin/highlight/highlight.esm.js @@ -0,0 +1,5 @@ +function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,a=new Array(t);n=74)&&(z=se.match(/Chrome\/(\d+)/))&&(W=z[1]);var de=W&&+W,ue=de,me=E,pe=!!Object.getOwnPropertySymbols&&!me((function(){return!String(Symbol())||!Symbol.sham&&ue&&ue<41})),ge=pe&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,Ee=p,Se=g.exports,be=Z,Te=te,fe=pe,Ce=ge,Ne=Se("wks"),Re=Ee.Symbol,ve=Ce?Re:Re&&Re.withoutSetter||Te,Oe=function(e){return be(Ne,e)&&(fe||"string"==typeof Ne[e])||(fe&&be(Re,e)?Ne[e]=Re[e]:Ne[e]=ve("Symbol."+e)),Ne[e]},he={};he[Oe("toStringTag")]="z";var ye="[object z]"===String(he),Ie={exports:{}},Ae=V,De=Function.toString;"function"!=typeof Ae.inspectSource&&(Ae.inspectSource=function(e){return De.call(e)});var Me,Le,we,xe=Ae.inspectSource,Pe=xe,ke=p.WeakMap,Ue="function"==typeof ke&&/native code/.test(Pe(ke)),Fe=g.exports,Be=te,Ge=Fe("keys"),Ye=function(e){return Ge[e]||(Ge[e]=Be(e))},He={},Ve=Ue,qe=T,ze=F,We=Z,$e=V,Qe=Ye,Ke=He,je=p.WeakMap;if(Ve||$e.state){var Xe=$e.state||($e.state=new je),Ze=Xe.get,Je=Xe.has,et=Xe.set;Me=function(e,t){if(Je.call(Xe,e))throw new TypeError("Object already initialized");return t.facade=e,et.call(Xe,e,t),t},Le=function(e){return Ze.call(Xe,e)||{}},we=function(e){return Je.call(Xe,e)}}else{var tt=Qe("state");Ke[tt]=!0,Me=function(e,t){if(We(e,tt))throw new TypeError("Object already initialized");return t.facade=e,ze(e,tt,t),t},Le=function(e){return We(e,tt)?e[tt]:{}},we=function(e){return We(e,tt)}}var nt={set:Me,get:Le,has:we,enforce:function(e){return we(e)?Le(e):Me(e,{})},getterFor:function(e){return function(t){var n;if(!qe(t)||(n=Le(t)).type!==e)throw TypeError("Incompatible receiver, "+e+" required");return n}}},at=p,rt=F,it=Z,ot=Y,st=xe,lt=nt.get,ct=nt.enforce,_t=String(String).split("String");(Ie.exports=function(e,t,n,a){var r,i=!!a&&!!a.unsafe,o=!!a&&!!a.enumerable,s=!!a&&!!a.noTargetGet;"function"==typeof n&&("string"!=typeof t||it(n,"name")||rt(n,"name",t),(r=ct(n)).source||(r.source=_t.join("string"==typeof t?t:""))),e!==at?(i?!s&&e[t]&&(o=!0):delete e[t],o?e[t]=n:rt(e,t,n)):o?e[t]=n:ot(t,n)})(Function.prototype,"toString",(function(){return"function"==typeof this&<(this).source||st(this)}));var dt={}.toString,ut=function(e){return dt.call(e).slice(8,-1)},mt=ye,pt=ut,gt=Oe("toStringTag"),Et="Arguments"==pt(function(){return arguments}()),St=mt?pt:function(e){var t,n,a;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(n=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),gt))?n:Et?pt(t):"Object"==(a=pt(t))&&"function"==typeof t.callee?"Arguments":a},bt=St,Tt=ye?{}.toString:function(){return"[object "+bt(this)+"]"},ft=ye,Ct=Ie.exports,Nt=Tt;ft||Ct(Object.prototype,"toString",Nt,{unsafe:!0});var Rt=y,vt=function(){var e=Rt(this),t="";return e.global&&(t+="g"),e.ignoreCase&&(t+="i"),e.multiline&&(t+="m"),e.dotAll&&(t+="s"),e.unicode&&(t+="u"),e.sticky&&(t+="y"),t},Ot=Ie.exports,ht=y,yt=E,It=vt,At=RegExp.prototype,Dt=At.toString,Mt=yt((function(){return"/a/b"!=Dt.call({source:"a",flags:"b"})})),Lt="toString"!=Dt.name;(Mt||Lt)&&Ot(RegExp.prototype,"toString",(function(){var e=ht(this),t=String(e.source),n=e.flags;return"/"+t+"/"+String(void 0===n&&e instanceof RegExp&&!("flags"in At)?It.call(e):n)}),{unsafe:!0});var wt={},xt={},Pt={}.propertyIsEnumerable,kt=Object.getOwnPropertyDescriptor,Ut=kt&&!Pt.call({1:2},1);xt.f=Ut?function(e){var t=kt(this,e);return!!t&&t.enumerable}:Pt;var Ft=ut,Bt="".split,Gt=E((function(){return!Object("z").propertyIsEnumerable(0)}))?function(e){return"String"==Ft(e)?Bt.call(e,""):Object(e)}:Object,Yt=Gt,Ht=$,Vt=function(e){return Yt(Ht(e))},qt=S,zt=xt,Wt=P,$t=Vt,Qt=A,Kt=Z,jt=O,Xt=Object.getOwnPropertyDescriptor;wt.f=qt?Xt:function(e,t){if(e=$t(e),t=Qt(t,!0),jt)try{return Xt(e,t)}catch(e){}if(Kt(e,t))return Wt(!zt.f.call(e,t),e[t])};var Zt={},Jt=Math.ceil,en=Math.floor,tn=function(e){return isNaN(e=+e)?0:(e>0?en:Jt)(e)},nn=tn,an=Math.min,rn=function(e){return e>0?an(nn(e),9007199254740991):0},on=tn,sn=Math.max,ln=Math.min,cn=function(e,t){var n=on(e);return n<0?sn(n+t,0):ln(n,t)},_n=Vt,dn=rn,un=cn,mn=function(e){return function(t,n,a){var r,i=_n(t),o=dn(i.length),s=un(a,o);if(e&&n!=n){for(;o>s;)if((r=i[s++])!=r)return!0}else for(;o>s;s++)if((e||s in i)&&i[s]===n)return e||s||0;return!e&&-1}},pn={includes:mn(!0),indexOf:mn(!1)},gn=Z,En=Vt,Sn=pn.indexOf,bn=He,Tn=function(e,t){var n,a=En(e),r=0,i=[];for(n in a)!gn(bn,n)&&gn(a,n)&&i.push(n);for(;t.length>r;)gn(a,n=t[r++])&&(~Sn(i,n)||i.push(n));return i},fn=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],Cn=Tn,Nn=fn.concat("length","prototype");Zt.f=Object.getOwnPropertyNames||function(e){return Cn(e,Nn)};var Rn={};Rn.f=Object.getOwnPropertySymbols;var vn=Zt,On=Rn,hn=y,yn=oe("Reflect","ownKeys")||function(e){var t=vn.f(hn(e)),n=On.f;return n?t.concat(n(e)):t},In=Z,An=yn,Dn=wt,Mn=b,Ln=function(e,t){for(var n=An(t),a=Mn.f,r=Dn.f,i=0;i=51||!ta((function(){var t=[];return(t.constructor={})[aa]=function(){return{foo:1}},1!==t[e](Boolean).foo}))},ia=Qn,oa=T,sa=jn,la=cn,ca=rn,_a=Vt,da=ea,ua=Oe,ma=ra("slice"),pa=ua("species"),ga=[].slice,Ea=Math.max;ia({target:"Array",proto:!0,forced:!ma},{slice:function(e,t){var n,a,r,i=_a(this),o=ca(i.length),s=la(e,o),l=la(void 0===t?o:t,o);if(sa(i)&&("function"!=typeof(n=i.constructor)||n!==Array&&!sa(n.prototype)?oa(n)&&null===(n=n[pa])&&(n=void 0):n=void 0,n===Array||void 0===n))return ga.call(i,s,l);for(a=new(void 0===n?Array:n)(Ea(l-s,0)),r=0;si;)Ia.f(e,n=a[i++],t[n]);return e},La=oe("document","documentElement"),wa=y,xa=Ma,Pa=fn,ka=He,Ua=La,Fa=R,Ba=Ye("IE_PROTO"),Ga=function(){},Ya=function(e){return"' ); + + var leadingWs = text.match( /^\n?(\s*)/ )[1].length, + leadingTabs = text.match( /^\n?(\t*)/ )[1].length; + + if( leadingTabs > 0 ) { + text = text.replace( new RegExp('\\n?\\t{' + leadingTabs + '}','g'), '\n' ); + } + else if( leadingWs > 1 ) { + text = text.replace( new RegExp('\\n? {' + leadingWs + '}', 'g'), '\n' ); + } + + return text; + + } + + /** + * Given a markdown slide section element, this will + * return all arguments that aren't related to markdown + * parsing. Used to forward any other user-defined arguments + * to the output markdown slide. + */ + function getForwardedAttributes( section ) { + + var attributes = section.attributes; + var result = []; + + for( var i = 0, len = attributes.length; i < len; i++ ) { + var name = attributes[i].name, + value = attributes[i].value; + + // disregard attributes that are used for markdown loading/parsing + if( /data\-(markdown|separator|vertical|notes)/gi.test( name ) ) continue; + + if( value ) { + result.push( name + '="' + value + '"' ); + } + else { + result.push( name ); + } + } + + return result.join( ' ' ); + + } + + /** + * Inspects the given options and fills out default + * values for what's not defined. + */ + function getSlidifyOptions( options ) { + + options = options || {}; + options.separator = options.separator || DEFAULT_SLIDE_SEPARATOR; + options.notesSeparator = options.notesSeparator || DEFAULT_NOTES_SEPARATOR; + options.attributes = options.attributes || ''; + + return options; + + } + + /** + * Helper function for constructing a markdown slide. + */ + function createMarkdownSlide( content, options ) { + + options = getSlidifyOptions( options ); + + var notesMatch = content.split( new RegExp( options.notesSeparator, 'mgi' ) ); + + if( notesMatch.length === 2 ) { + content = notesMatch[0] + ''; + } + + // prevent script end tags in the content from interfering + // with parsing + content = content.replace( /<\/script>/g, SCRIPT_END_PLACEHOLDER ); + + return ''; + + } + + /** + * Parses a data string into multiple slides based + * on the passed in separator arguments. + */ + function slidify( markdown, options ) { + + options = getSlidifyOptions( options ); + + var separatorRegex = new RegExp( options.separator + ( options.verticalSeparator ? '|' + options.verticalSeparator : '' ), 'mg' ), + horizontalSeparatorRegex = new RegExp( options.separator ); + + var matches, + lastIndex = 0, + isHorizontal, + wasHorizontal = true, + content, + sectionStack = []; + + // iterate until all blocks between separators are stacked up + while( matches = separatorRegex.exec( markdown ) ) { + var notes = null; + + // determine direction (horizontal by default) + isHorizontal = horizontalSeparatorRegex.test( matches[0] ); + + if( !isHorizontal && wasHorizontal ) { + // create vertical stack + sectionStack.push( [] ); + } + + // pluck slide content from markdown input + content = markdown.substring( lastIndex, matches.index ); + + if( isHorizontal && wasHorizontal ) { + // add to horizontal stack + sectionStack.push( content ); + } + else { + // add to vertical stack + sectionStack[sectionStack.length-1].push( content ); + } + + lastIndex = separatorRegex.lastIndex; + wasHorizontal = isHorizontal; + } + + // add the remaining slide + ( wasHorizontal ? sectionStack : sectionStack[sectionStack.length-1] ).push( markdown.substring( lastIndex ) ); + + var markdownSections = ''; + + // flatten the hierarchical stack, and insert
tags + for( var i = 0, len = sectionStack.length; i < len; i++ ) { + // vertical + if( sectionStack[i] instanceof Array ) { + markdownSections += '
'; + + sectionStack[i].forEach( function( child ) { + markdownSections += '
' + createMarkdownSlide( child, options ) + '
'; + } ); + + markdownSections += '
'; + } + else { + markdownSections += '
' + createMarkdownSlide( sectionStack[i], options ) + '
'; + } + } + + return markdownSections; + + } + + /** + * Parses any current data-markdown slides, splits + * multi-slide markdown into separate sections and + * handles loading of external markdown. + */ + function processSlides( scope ) { + + return new Promise( function( resolve ) { + + var externalPromises = []; + + [].slice.call( scope.querySelectorAll( 'section[data-markdown]:not([data-markdown-parsed])') ).forEach( function( section, i ) { + + if( section.getAttribute( 'data-markdown' ).length ) { + + externalPromises.push( loadExternalMarkdown( section ).then( + + // Finished loading external file + function( xhr, url ) { + section.outerHTML = slidify( xhr.responseText, { + separator: section.getAttribute( 'data-separator' ), + verticalSeparator: section.getAttribute( 'data-separator-vertical' ), + notesSeparator: section.getAttribute( 'data-separator-notes' ), + attributes: getForwardedAttributes( section ) + }); + }, + + // Failed to load markdown + function( xhr, url ) { + section.outerHTML = '
' + + 'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' + + 'Check your browser\'s JavaScript console for more details.' + + '

Remember that you need to serve the presentation HTML from a HTTP server.

' + + '
'; + } + + ) ); + + } + else { + + section.outerHTML = slidify( getMarkdownFromSlide( section ), { + separator: section.getAttribute( 'data-separator' ), + verticalSeparator: section.getAttribute( 'data-separator-vertical' ), + notesSeparator: section.getAttribute( 'data-separator-notes' ), + attributes: getForwardedAttributes( section ) + }); + + } + + }); + + Promise.all( externalPromises ).then( resolve ); + + } ); + + } + + function loadExternalMarkdown( section ) { + + return new Promise( function( resolve, reject ) { + + var xhr = new XMLHttpRequest(), + url = section.getAttribute( 'data-markdown' ); + + var datacharset = section.getAttribute( 'data-charset' ); + + // see https://developer.mozilla.org/en-US/docs/Web/API/element.getAttribute#Notes + if( datacharset != null && datacharset != '' ) { + xhr.overrideMimeType( 'text/html; charset=' + datacharset ); + } + + xhr.onreadystatechange = function( section, xhr ) { + if( xhr.readyState === 4 ) { + // file protocol yields status code 0 (useful for local debug, mobile applications etc.) + if ( ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status === 0 ) { + + resolve( xhr, url ); + + } + else { + + reject( xhr, url ); + + } + } + }.bind( this, section, xhr ); + + xhr.open( 'GET', url, true ); + + try { + xhr.send(); + } + catch ( e ) { + console.warn( 'Failed to get the Markdown file ' + url + '. Make sure that the presentation and the file are served by a HTTP server and the file can be found there. ' + e ); + resolve( xhr, url ); + } + + } ); + + } + + /** + * Check if a node value has the attributes pattern. + * If yes, extract it and add that value as one or several attributes + * to the target element. + * + * You need Cache Killer on Chrome to see the effect on any FOM transformation + * directly on refresh (F5) + * http://stackoverflow.com/questions/5690269/disabling-chrome-cache-for-website-development/7000899#answer-11786277 + */ + function addAttributeInElement( node, elementTarget, separator ) { + + var mardownClassesInElementsRegex = new RegExp( separator, 'mg' ); + var mardownClassRegex = new RegExp( "([^\"= ]+?)=\"([^\"]+?)\"|(data-[^\"= ]+?)(?=[\" ])", 'mg' ); + var nodeValue = node.nodeValue; + var matches, + matchesClass; + if( matches = mardownClassesInElementsRegex.exec( nodeValue ) ) { + + var classes = matches[1]; + nodeValue = nodeValue.substring( 0, matches.index ) + nodeValue.substring( mardownClassesInElementsRegex.lastIndex ); + node.nodeValue = nodeValue; + while( matchesClass = mardownClassRegex.exec( classes ) ) { + if( matchesClass[2] ) { + elementTarget.setAttribute( matchesClass[1], matchesClass[2] ); + } else { + elementTarget.setAttribute( matchesClass[3], "" ); + } + } + return true; + } + return false; + } + + /** + * Add attributes to the parent element of a text node, + * or the element of an attribute node. + */ + function addAttributes( section, element, previousElement, separatorElementAttributes, separatorSectionAttributes ) { + + if ( element != null && element.childNodes != undefined && element.childNodes.length > 0 ) { + var previousParentElement = element; + for( var i = 0; i < element.childNodes.length; i++ ) { + var childElement = element.childNodes[i]; + if ( i > 0 ) { + var j = i - 1; + while ( j >= 0 ) { + var aPreviousChildElement = element.childNodes[j]; + if ( typeof aPreviousChildElement.setAttribute == 'function' && aPreviousChildElement.tagName != "BR" ) { + previousParentElement = aPreviousChildElement; + break; + } + j = j - 1; + } + } + var parentSection = section; + if( childElement.nodeName == "section" ) { + parentSection = childElement ; + previousParentElement = childElement ; + } + if ( typeof childElement.setAttribute == 'function' || childElement.nodeType == Node.COMMENT_NODE ) { + addAttributes( parentSection, childElement, previousParentElement, separatorElementAttributes, separatorSectionAttributes ); + } + } + } + + if ( element.nodeType == Node.COMMENT_NODE ) { + if ( addAttributeInElement( element, previousElement, separatorElementAttributes ) == false ) { + addAttributeInElement( element, section, separatorSectionAttributes ); + } + } + } + + /** + * Converts any current data-markdown slides in the + * DOM to HTML. + */ + function convertSlides() { + + var sections = deck.getRevealElement().querySelectorAll( '[data-markdown]:not([data-markdown-parsed])'); + + [].slice.call( sections ).forEach( function( section ) { + + section.setAttribute( 'data-markdown-parsed', true ) + + var notes = section.querySelector( 'aside.notes' ); + var markdown = getMarkdownFromSlide( section ); + + section.innerHTML = marked( markdown ); + addAttributes( section, section, null, section.getAttribute( 'data-element-attributes' ) || + section.parentNode.getAttribute( 'data-element-attributes' ) || + DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR, + section.getAttribute( 'data-attributes' ) || + section.parentNode.getAttribute( 'data-attributes' ) || + DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR); + + // If there were notes, we need to re-add them after + // having overwritten the section's HTML + if( notes ) { + section.appendChild( notes ); + } + + } ); + + return Promise.resolve(); + + } + + function escapeForHTML( input ) { + + return input.replace( /([&<>'"])/g, char => HTML_ESCAPE_MAP[char] ); + + } + + return { + id: 'markdown', + + /** + * Starts processing and converting Markdown within the + * current reveal.js deck. + */ + init: function( reveal ) { + + deck = reveal; + + let { renderer, animateLists, ...markedOptions } = deck.getConfig().markdown || {}; + + if( !renderer ) { + renderer = new marked.Renderer(); + + renderer.code = ( code, language ) => { + + // Off by default + let lineNumbers = ''; + + // Users can opt in to show line numbers and highlight + // specific lines. + // ```javascript [] show line numbers + // ```javascript [1,4-8] highlights lines 1 and 4-8 + if( CODE_LINE_NUMBER_REGEX.test( language ) ) { + lineNumbers = language.match( CODE_LINE_NUMBER_REGEX )[1].trim(); + lineNumbers = `data-line-numbers="${lineNumbers}"`; + language = language.replace( CODE_LINE_NUMBER_REGEX, '' ).trim(); + } + + // Escape before this gets injected into the DOM to + // avoid having the HTML parser alter our code before + // highlight.js is able to read it + code = escapeForHTML( code ); + + return `
${code}
`; + }; + } + + if( animateLists === true ) { + renderer.listitem = text => `
  • ${text}
  • `; + } + + marked.setOptions( { + renderer, + ...markedOptions + } ); + + return processSlides( deck.getRevealElement() ).then( convertSlides ); + + }, + + // TODO: Do these belong in the API? + processSlides: processSlides, + convertSlides: convertSlides, + slidify: slidify, + marked: marked + } + +}; + +export default Plugin; diff --git a/mstreams/presentation/plugin/math/katex.js b/mstreams/presentation/plugin/math/katex.js new file mode 100644 index 0000000..a8b47c4 --- /dev/null +++ b/mstreams/presentation/plugin/math/katex.js @@ -0,0 +1,96 @@ +/** + * A plugin which enables rendering of math equations inside + * of reveal.js slides. Essentially a thin wrapper for KaTeX. + * + * @author Hakim El Hattab + * @author Gerhard Burger + */ +export const KaTeX = () => { + let deck; + + let defaultOptions = { + version: 'latest', + delimiters: [ + {left: '$$', right: '$$', display: true}, // Note: $$ has to come before $ + {left: '$', right: '$', display: false}, + {left: '\\(', right: '\\)', display: false}, + {left: '\\[', right: '\\]', display: true} + ], + ignoredTags: ['script', 'noscript', 'style', 'textarea', 'pre'] + } + + const loadCss = src => { + let link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = src; + document.head.appendChild(link); + }; + + /** + * Loads a JavaScript file and returns a Promise for when it is loaded + * Credits: https://aaronsmith.online/easily-load-an-external-script-using-javascript/ + */ + const loadScript = src => { + return new Promise((resolve, reject) => { + const script = document.createElement('script') + script.type = 'text/javascript' + script.onload = resolve + script.onerror = reject + script.src = src + document.head.append(script) + }) + }; + + async function loadScripts(urls) { + for(const url of urls) { + await loadScript(url); + } + } + + return { + id: 'katex', + + init: function (reveal) { + + deck = reveal; + + let revealOptions = deck.getConfig().katex || {}; + + let options = {...defaultOptions, ...revealOptions}; + const {local, version, extensions, ...katexOptions} = options; + + let baseUrl = options.local || 'https://cdn.jsdelivr.net/npm/katex'; + let versionString = options.local ? '' : '@' + options.version; + + let cssUrl = baseUrl + versionString + '/dist/katex.min.css'; + let katexUrl = baseUrl + versionString + '/dist/katex.min.js'; + let mhchemUrl = baseUrl + versionString + '/dist/contrib/mhchem.min.js' + let karUrl = baseUrl + versionString + '/dist/contrib/auto-render.min.js'; + + let katexScripts = [katexUrl]; + if(options.extensions && options.extensions.includes("mhchem")) { + katexScripts.push(mhchemUrl); + } + katexScripts.push(karUrl); + + const renderMath = () => { + renderMathInElement(reveal.getSlidesElement(), katexOptions); + deck.layout(); + } + + loadCss(cssUrl); + + // For some reason dynamically loading with defer attribute doesn't result in the expected behavior, the below code does + loadScripts(katexScripts).then(() => { + if( deck.isReady() ) { + renderMath(); + } + else { + deck.on( 'ready', renderMath.bind( this ) ); + } + }); + + } + } + +}; diff --git a/mstreams/presentation/plugin/math/math.esm.js b/mstreams/presentation/plugin/math/math.esm.js new file mode 100644 index 0000000..a0d1468 --- /dev/null +++ b/mstreams/presentation/plugin/math/math.esm.js @@ -0,0 +1 @@ +var t="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},e=function(t){return t&&t.Math==Math&&t},n=e("object"==typeof globalThis&&globalThis)||e("object"==typeof window&&window)||e("object"==typeof self&&self)||e("object"==typeof t&&t)||function(){return this}()||Function("return this")(),r={},o=function(t){try{return!!t()}catch(t){return!0}},i=!o((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]})),a={},c={}.propertyIsEnumerable,u=Object.getOwnPropertyDescriptor,f=u&&!c.call({1:2},1);a.f=f?function(t){var e=u(this,t);return!!e&&e.enumerable}:c;var s=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}},l={}.toString,p=function(t){return l.call(t).slice(8,-1)},h=p,v="".split,d=o((function(){return!Object("z").propertyIsEnumerable(0)}))?function(t){return"String"==h(t)?v.call(t,""):Object(t)}:Object,y=function(t){if(null==t)throw TypeError("Can't call method on "+t);return t},g=d,m=y,b=function(t){return g(m(t))},w=function(t){return"object"==typeof t?null!==t:"function"==typeof t},j=w,x=function(t,e){if(!j(t))return t;var n,r;if(e&&"function"==typeof(n=t.toString)&&!j(r=n.call(t)))return r;if("function"==typeof(n=t.valueOf)&&!j(r=n.call(t)))return r;if(!e&&"function"==typeof(n=t.toString)&&!j(r=n.call(t)))return r;throw TypeError("Can't convert object to primitive value")},O=y,E=function(t){return Object(O(t))},S=E,T={}.hasOwnProperty,P=function(t,e){return T.call(S(t),e)},_=w,k=n.document,L=_(k)&&_(k.createElement),M=function(t){return L?k.createElement(t):{}},A=M,I=!i&&!o((function(){return 7!=Object.defineProperty(A("div"),"a",{get:function(){return 7}}).a})),R=i,C=a,N=s,F=b,J=x,D=P,$=I,G=Object.getOwnPropertyDescriptor;r.f=R?G:function(t,e){if(t=F(t),e=J(e,!0),$)try{return G(t,e)}catch(t){}if(D(t,e))return N(!C.f.call(t,e),t[e])};var H={},z=w,W=function(t){if(!z(t))throw TypeError(String(t)+" is not an object");return t},q=i,U=I,K=W,Q=x,X=Object.defineProperty;H.f=q?X:function(t,e,n){if(K(t),e=Q(e,!0),K(n),U)try{return X(t,e,n)}catch(t){}if("get"in n||"set"in n)throw TypeError("Accessors not supported");return"value"in n&&(t[e]=n.value),t};var Y=H,B=s,V=i?function(t,e,n){return Y.f(t,e,B(1,n))}:function(t,e,n){return t[e]=n,t},Z={exports:{}},tt=n,et=V,nt=function(t,e){try{et(tt,t,e)}catch(n){tt[t]=e}return e},rt=nt,ot=n["__core-js_shared__"]||rt("__core-js_shared__",{}),it=ot,at=Function.toString;"function"!=typeof it.inspectSource&&(it.inspectSource=function(t){return at.call(t)});var ct=it.inspectSource,ut=ct,ft=n.WeakMap,st="function"==typeof ft&&/native code/.test(ut(ft)),lt={exports:{}},pt=ot;(lt.exports=function(t,e){return pt[t]||(pt[t]=void 0!==e?e:{})})("versions",[]).push({version:"3.12.1",mode:"global",copyright:"© 2021 Denis Pushkarev (zloirock.ru)"});var ht,vt,dt,yt=0,gt=Math.random(),mt=function(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++yt+gt).toString(36)},bt=lt.exports,wt=mt,jt=bt("keys"),xt=function(t){return jt[t]||(jt[t]=wt(t))},Ot={},Et=st,St=w,Tt=V,Pt=P,_t=ot,kt=xt,Lt=Ot,Mt=n.WeakMap;if(Et||_t.state){var At=_t.state||(_t.state=new Mt),It=At.get,Rt=At.has,Ct=At.set;ht=function(t,e){if(Rt.call(At,t))throw new TypeError("Object already initialized");return e.facade=t,Ct.call(At,t,e),e},vt=function(t){return It.call(At,t)||{}},dt=function(t){return Rt.call(At,t)}}else{var Nt=kt("state");Lt[Nt]=!0,ht=function(t,e){if(Pt(t,Nt))throw new TypeError("Object already initialized");return e.facade=t,Tt(t,Nt,e),e},vt=function(t){return Pt(t,Nt)?t[Nt]:{}},dt=function(t){return Pt(t,Nt)}}var Ft={set:ht,get:vt,has:dt,enforce:function(t){return dt(t)?vt(t):ht(t,{})},getterFor:function(t){return function(e){var n;if(!St(e)||(n=vt(e)).type!==t)throw TypeError("Incompatible receiver, "+t+" required");return n}}},Jt=n,Dt=V,$t=P,Gt=nt,Ht=ct,zt=Ft.get,Wt=Ft.enforce,qt=String(String).split("String");(Z.exports=function(t,e,n,r){var o,i=!!r&&!!r.unsafe,a=!!r&&!!r.enumerable,c=!!r&&!!r.noTargetGet;"function"==typeof n&&("string"!=typeof e||$t(n,"name")||Dt(n,"name",e),(o=Wt(n)).source||(o.source=qt.join("string"==typeof e?e:""))),t!==Jt?(i?!c&&t[e]&&(a=!0):delete t[e],a?t[e]=n:Dt(t,e,n)):a?t[e]=n:Gt(e,n)})(Function.prototype,"toString",(function(){return"function"==typeof this&&zt(this).source||Ht(this)}));var Ut=n,Kt=n,Qt=function(t){return"function"==typeof t?t:void 0},Xt=function(t,e){return arguments.length<2?Qt(Ut[t])||Qt(Kt[t]):Ut[t]&&Ut[t][e]||Kt[t]&&Kt[t][e]},Yt={},Bt=Math.ceil,Vt=Math.floor,Zt=function(t){return isNaN(t=+t)?0:(t>0?Vt:Bt)(t)},te=Zt,ee=Math.min,ne=function(t){return t>0?ee(te(t),9007199254740991):0},re=Zt,oe=Math.max,ie=Math.min,ae=b,ce=ne,ue=function(t,e){var n=re(t);return n<0?oe(n+e,0):ie(n,e)},fe=function(t){return function(e,n,r){var o,i=ae(e),a=ce(i.length),c=ue(r,a);if(t&&n!=n){for(;a>c;)if((o=i[c++])!=o)return!0}else for(;a>c;c++)if((t||c in i)&&i[c]===n)return t||c||0;return!t&&-1}},se={includes:fe(!0),indexOf:fe(!1)},le=P,pe=b,he=se.indexOf,ve=Ot,de=function(t,e){var n,r=pe(t),o=0,i=[];for(n in r)!le(ve,n)&&le(r,n)&&i.push(n);for(;e.length>o;)le(r,n=e[o++])&&(~he(i,n)||i.push(n));return i},ye=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],ge=de,me=ye.concat("length","prototype");Yt.f=Object.getOwnPropertyNames||function(t){return ge(t,me)};var be={};be.f=Object.getOwnPropertySymbols;var we=Yt,je=be,xe=W,Oe=Xt("Reflect","ownKeys")||function(t){var e=we.f(xe(t)),n=je.f;return n?e.concat(n(t)):e},Ee=P,Se=Oe,Te=r,Pe=H,_e=o,ke=/#|\.prototype\./,Le=function(t,e){var n=Ae[Me(t)];return n==Re||n!=Ie&&("function"==typeof e?_e(e):!!e)},Me=Le.normalize=function(t){return String(t).replace(ke,".").toLowerCase()},Ae=Le.data={},Ie=Le.NATIVE="N",Re=Le.POLYFILL="P",Ce=Le,Ne=n,Fe=r.f,Je=V,De=Z.exports,$e=nt,Ge=function(t,e){for(var n=Se(e),r=Pe.f,o=Te.f,i=0;io;)for(var c,u=Ze(arguments[o++]),f=i?Xe(u).concat(i(u)):Xe(u),s=f.length,l=0;s>l;)c=f[l++],Ke&&!a.call(u,c)||(n[c]=u[c]);return n}:tn;function rn(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,r)}return n}function on(t){for(var e=1;e=0||(o[n]=t[n]);return o}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function sn(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,r=new Array(e);n=t.length?{done:!0}:{done:!1,value:t[r++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,a=!0,c=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return a=t.done,t},e:function(t){c=!0,i=t},f:function(){try{a||null==n.return||n.return()}finally{if(c)throw i}}}}ze({target:"Object",stat:!0,forced:Object.assign!==nn},{assign:nn});!function(t){var e=function(t){var e,n=Object.prototype,r=n.hasOwnProperty,o="function"==typeof Symbol?Symbol:{},i=o.iterator||"@@iterator",a=o.asyncIterator||"@@asyncIterator",c=o.toStringTag||"@@toStringTag";function u(t,e,n){return Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}),t[e]}try{u({},"")}catch(t){u=function(t,e,n){return t[e]=n}}function f(t,e,n,r){var o=e&&e.prototype instanceof y?e:y,i=Object.create(o.prototype),a=new _(r||[]);return i._invoke=function(t,e,n){var r=l;return function(o,i){if(r===h)throw new Error("Generator is already running");if(r===v){if("throw"===o)throw i;return L()}for(n.method=o,n.arg=i;;){var a=n.delegate;if(a){var c=S(a,n);if(c){if(c===d)continue;return c}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if(r===l)throw r=v,n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);r=h;var u=s(t,e,n);if("normal"===u.type){if(r=n.done?v:p,u.arg===d)continue;return{value:u.arg,done:n.done}}"throw"===u.type&&(r=v,n.method="throw",n.arg=u.arg)}}}(t,n,a),i}function s(t,e,n){try{return{type:"normal",arg:t.call(e,n)}}catch(t){return{type:"throw",arg:t}}}t.wrap=f;var l="suspendedStart",p="suspendedYield",h="executing",v="completed",d={};function y(){}function g(){}function m(){}var b={};b[i]=function(){return this};var w=Object.getPrototypeOf,j=w&&w(w(k([])));j&&j!==n&&r.call(j,i)&&(b=j);var x=m.prototype=y.prototype=Object.create(b);function O(t){["next","throw","return"].forEach((function(e){u(t,e,(function(t){return this._invoke(e,t)}))}))}function E(t,e){function n(o,i,a,c){var u=s(t[o],t,i);if("throw"!==u.type){var f=u.arg,l=f.value;return l&&"object"==typeof l&&r.call(l,"__await")?e.resolve(l.__await).then((function(t){n("next",t,a,c)}),(function(t){n("throw",t,a,c)})):e.resolve(l).then((function(t){f.value=t,a(f)}),(function(t){return n("throw",t,a,c)}))}c(u.arg)}var o;this._invoke=function(t,r){function i(){return new e((function(e,o){n(t,r,e,o)}))}return o=o?o.then(i,i):i()}}function S(t,n){var r=t.iterator[n.method];if(r===e){if(n.delegate=null,"throw"===n.method){if(t.iterator.return&&(n.method="return",n.arg=e,S(t,n),"throw"===n.method))return d;n.method="throw",n.arg=new TypeError("The iterator does not provide a 'throw' method")}return d}var o=s(r,t.iterator,n.arg);if("throw"===o.type)return n.method="throw",n.arg=o.arg,n.delegate=null,d;var i=o.arg;return i?i.done?(n[t.resultName]=i.value,n.next=t.nextLoc,"return"!==n.method&&(n.method="next",n.arg=e),n.delegate=null,d):i:(n.method="throw",n.arg=new TypeError("iterator result is not an object"),n.delegate=null,d)}function T(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function P(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function _(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(T,this),this.reset(!0)}function k(t){if(t){var n=t[i];if(n)return n.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var o=-1,a=function n(){for(;++o=0;--i){var a=this.tryEntries[i],c=a.completion;if("root"===a.tryLoc)return o("end");if(a.tryLoc<=this.prev){var u=r.call(a,"catchLoc"),f=r.call(a,"finallyLoc");if(u&&f){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--e){var n=this.tryEntries[e];if(n.finallyLoc===t)return this.complete(n.completion,n.afterLoc),P(n),d}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n.tryLoc===t){var r=n.completion;if("throw"===r.type){var o=r.arg;P(n)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,n,r){return this.delegate={iterator:k(t),resultName:n,nextLoc:r},"next"===this.method&&(this.arg=e),d}},t}(t.exports);try{regeneratorRuntime=e}catch(t){Function("r","regeneratorRuntime = r")(e)}}({exports:{}});var pn,hn,vn=Xt("navigator","userAgent")||"",dn=vn,yn=n.process,gn=yn&&yn.versions,mn=gn&&gn.v8;mn?hn=(pn=mn.split("."))[0]<4?1:pn[0]+pn[1]:dn&&(!(pn=dn.match(/Edge\/(\d+)/))||pn[1]>=74)&&(pn=dn.match(/Chrome\/(\d+)/))&&(hn=pn[1]);var bn=hn&&+hn,wn=bn,jn=o,xn=!!Object.getOwnPropertySymbols&&!jn((function(){return!String(Symbol())||!Symbol.sham&&wn&&wn<41})),On=xn&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,En=n,Sn=lt.exports,Tn=P,Pn=mt,_n=xn,kn=On,Ln=Sn("wks"),Mn=En.Symbol,An=kn?Mn:Mn&&Mn.withoutSetter||Pn,In=function(t){return Tn(Ln,t)&&(_n||"string"==typeof Ln[t])||(_n&&Tn(Mn,t)?Ln[t]=Mn[t]:Ln[t]=An("Symbol."+t)),Ln[t]},Rn={};Rn[In("toStringTag")]="z";var Cn="[object z]"===String(Rn),Nn=Cn,Fn=p,Jn=In("toStringTag"),Dn="Arguments"==Fn(function(){return arguments}()),$n=Nn?Fn:function(t){var e,n,r;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(n=function(t,e){try{return t[e]}catch(t){}}(e=Object(t),Jn))?n:Dn?Fn(e):"Object"==(r=Fn(e))&&"function"==typeof e.callee?"Arguments":r},Gn=$n,Hn=Cn?{}.toString:function(){return"[object "+Gn(this)+"]"},zn=Cn,Wn=Z.exports,qn=Hn;zn||Wn(Object.prototype,"toString",qn,{unsafe:!0});var Un=n.Promise,Kn=Z.exports,Qn=w,Xn=W,Yn=function(t){if(!Qn(t)&&null!==t)throw TypeError("Can't set "+String(t)+" as a prototype");return t},Bn=Object.setPrototypeOf||("__proto__"in{}?function(){var t,e=!1,n={};try{(t=Object.getOwnPropertyDescriptor(Object.prototype,"__proto__").set).call(n,[]),e=n instanceof Array}catch(t){}return function(n,r){return Xn(n),Yn(r),e?t.call(n,r):n.__proto__=r,n}}():void 0),Vn=H.f,Zn=P,tr=In("toStringTag"),er=Xt,nr=H,rr=i,or=In("species"),ir=function(t){if("function"!=typeof t)throw TypeError(String(t)+" is not a function");return t},ar={},cr=ar,ur=In("iterator"),fr=Array.prototype,sr=ir,lr=function(t,e,n){if(sr(t),void 0===e)return t;switch(n){case 0:return function(){return t.call(e)};case 1:return function(n){return t.call(e,n)};case 2:return function(n,r){return t.call(e,n,r)};case 3:return function(n,r,o){return t.call(e,n,r,o)}}return function(){return t.apply(e,arguments)}},pr=$n,hr=ar,vr=In("iterator"),dr=W,yr=W,gr=function(t){return void 0!==t&&(cr.Array===t||fr[ur]===t)},mr=ne,br=lr,wr=function(t){if(null!=t)return t[vr]||t["@@iterator"]||hr[pr(t)]},jr=function(t){var e=t.return;if(void 0!==e)return dr(e.call(t)).value},xr=function(t,e){this.stopped=t,this.result=e},Or=In("iterator"),Er=!1;try{var Sr=0,Tr={next:function(){return{done:!!Sr++}},return:function(){Er=!0}};Tr[Or]=function(){return this},Array.from(Tr,(function(){throw 2}))}catch(t){}var Pr,_r,kr,Lr=W,Mr=ir,Ar=In("species"),Ir=Xt("document","documentElement"),Rr=/(?:iphone|ipod|ipad).*applewebkit/i.test(vn),Cr="process"==p(n.process),Nr=n,Fr=o,Jr=lr,Dr=Ir,$r=M,Gr=Rr,Hr=Cr,zr=Nr.location,Wr=Nr.setImmediate,qr=Nr.clearImmediate,Ur=Nr.process,Kr=Nr.MessageChannel,Qr=Nr.Dispatch,Xr=0,Yr={},Br=function(t){if(Yr.hasOwnProperty(t)){var e=Yr[t];delete Yr[t],e()}},Vr=function(t){return function(){Br(t)}},Zr=function(t){Br(t.data)},to=function(t){Nr.postMessage(t+"",zr.protocol+"//"+zr.host)};Wr&&qr||(Wr=function(t){for(var e=[],n=1;arguments.length>n;)e.push(arguments[n++]);return Yr[++Xr]=function(){("function"==typeof t?t:Function(t)).apply(void 0,e)},Pr(Xr),Xr},qr=function(t){delete Yr[t]},Hr?Pr=function(t){Ur.nextTick(Vr(t))}:Qr&&Qr.now?Pr=function(t){Qr.now(Vr(t))}:Kr&&!Gr?(kr=(_r=new Kr).port2,_r.port1.onmessage=Zr,Pr=Jr(kr.postMessage,kr,1)):Nr.addEventListener&&"function"==typeof postMessage&&!Nr.importScripts&&zr&&"file:"!==zr.protocol&&!Fr(to)?(Pr=to,Nr.addEventListener("message",Zr,!1)):Pr="onreadystatechange"in $r("script")?function(t){Dr.appendChild($r("script")).onreadystatechange=function(){Dr.removeChild(this),Br(t)}}:function(t){setTimeout(Vr(t),0)});var eo,no,ro,oo,io,ao,co,uo,fo={set:Wr,clear:qr},so=/web0s(?!.*chrome)/i.test(vn),lo=n,po=r.f,ho=fo.set,vo=Rr,yo=so,go=Cr,mo=lo.MutationObserver||lo.WebKitMutationObserver,bo=lo.document,wo=lo.process,jo=lo.Promise,xo=po(lo,"queueMicrotask"),Oo=xo&&xo.value;Oo||(eo=function(){var t,e;for(go&&(t=wo.domain)&&t.exit();no;){e=no.fn,no=no.next;try{e()}catch(t){throw no?oo():ro=void 0,t}}ro=void 0,t&&t.enter()},vo||go||yo||!mo||!bo?jo&&jo.resolve?((co=jo.resolve(void 0)).constructor=jo,uo=co.then,oo=function(){uo.call(co,eo)}):oo=go?function(){wo.nextTick(eo)}:function(){ho.call(lo,eo)}:(io=!0,ao=bo.createTextNode(""),new mo(eo).observe(ao,{characterData:!0}),oo=function(){ao.data=io=!io}));var Eo=Oo||function(t){var e={fn:t,next:void 0};ro&&(ro.next=e),no||(no=e,oo()),ro=e},So={},To=ir,Po=function(t){var e,n;this.promise=new t((function(t,r){if(void 0!==e||void 0!==n)throw TypeError("Bad Promise constructor");e=t,n=r})),this.resolve=To(e),this.reject=To(n)};So.f=function(t){return new Po(t)};var _o,ko,Lo,Mo,Ao=W,Io=w,Ro=So,Co=n,No="object"==typeof window,Fo=ze,Jo=n,Do=Xt,$o=Un,Go=Z.exports,Ho=function(t,e,n){for(var r in e)Kn(t,r,e[r],n);return t},zo=Bn,Wo=function(t,e,n){t&&!Zn(t=n?t:t.prototype,tr)&&Vn(t,tr,{configurable:!0,value:e})},qo=function(t){var e=er(t),n=nr.f;rr&&e&&!e[or]&&n(e,or,{configurable:!0,get:function(){return this}})},Uo=w,Ko=ir,Qo=function(t,e,n){if(!(t instanceof e))throw TypeError("Incorrect "+(n?n+" ":"")+"invocation");return t},Xo=ct,Yo=function(t,e,n){var r,o,i,a,c,u,f,s=n&&n.that,l=!(!n||!n.AS_ENTRIES),p=!(!n||!n.IS_ITERATOR),h=!(!n||!n.INTERRUPTED),v=br(e,s,1+l+h),d=function(t){return r&&jr(r),new xr(!0,t)},y=function(t){return l?(yr(t),h?v(t[0],t[1],d):v(t[0],t[1])):h?v(t,d):v(t)};if(p)r=t;else{if("function"!=typeof(o=wr(t)))throw TypeError("Target is not iterable");if(gr(o)){for(i=0,a=mr(t.length);a>i;i++)if((c=y(t[i]))&&c instanceof xr)return c;return new xr(!1)}r=o.call(t)}for(u=r.next;!(f=u.call(r)).done;){try{c=y(f.value)}catch(t){throw jr(r),t}if("object"==typeof c&&c&&c instanceof xr)return c}return new xr(!1)},Bo=function(t,e){if(!e&&!Er)return!1;var n=!1;try{var r={};r[Or]=function(){return{next:function(){return{done:n=!0}}}},t(r)}catch(t){}return n},Vo=function(t,e){var n,r=Lr(t).constructor;return void 0===r||null==(n=Lr(r)[Ar])?e:Mr(n)},Zo=fo.set,ti=Eo,ei=function(t,e){if(Ao(t),Io(e)&&e.constructor===t)return e;var n=Ro.f(t);return(0,n.resolve)(e),n.promise},ni=function(t,e){var n=Co.console;n&&n.error&&(1===arguments.length?n.error(t):n.error(t,e))},ri=So,oi=function(t){try{return{error:!1,value:t()}}catch(t){return{error:!0,value:t}}},ii=Ft,ai=Ce,ci=No,ui=Cr,fi=bn,si=In("species"),li="Promise",pi=ii.get,hi=ii.set,vi=ii.getterFor(li),di=$o&&$o.prototype,yi=$o,gi=di,mi=Jo.TypeError,bi=Jo.document,wi=Jo.process,ji=ri.f,xi=ji,Oi=!!(bi&&bi.createEvent&&Jo.dispatchEvent),Ei="function"==typeof PromiseRejectionEvent,Si=!1,Ti=ai(li,(function(){var t=Xo(yi)!==String(yi);if(!t&&66===fi)return!0;if(fi>=51&&/native code/.test(yi))return!1;var e=new yi((function(t){t(1)})),n=function(t){t((function(){}),(function(){}))};return(e.constructor={})[si]=n,!(Si=e.then((function(){}))instanceof n)||!t&&ci&&!Ei})),Pi=Ti||!Bo((function(t){yi.all(t).catch((function(){}))})),_i=function(t){var e;return!(!Uo(t)||"function"!=typeof(e=t.then))&&e},ki=function(t,e){if(!t.notified){t.notified=!0;var n=t.reactions;ti((function(){for(var r=t.value,o=1==t.state,i=0;n.length>i;){var a,c,u,f=n[i++],s=o?f.ok:f.fail,l=f.resolve,p=f.reject,h=f.domain;try{s?(o||(2===t.rejection&&Ii(t),t.rejection=1),!0===s?a=r:(h&&h.enter(),a=s(r),h&&(h.exit(),u=!0)),a===f.promise?p(mi("Promise-chain cycle")):(c=_i(a))?c.call(a,l,p):l(a)):p(r)}catch(t){h&&!u&&h.exit(),p(t)}}t.reactions=[],t.notified=!1,e&&!t.rejection&&Mi(t)}))}},Li=function(t,e,n){var r,o;Oi?((r=bi.createEvent("Event")).promise=e,r.reason=n,r.initEvent(t,!1,!0),Jo.dispatchEvent(r)):r={promise:e,reason:n},!Ei&&(o=Jo["on"+t])?o(r):"unhandledrejection"===t&&ni("Unhandled promise rejection",n)},Mi=function(t){Zo.call(Jo,(function(){var e,n=t.facade,r=t.value;if(Ai(t)&&(e=oi((function(){ui?wi.emit("unhandledRejection",r,n):Li("unhandledrejection",n,r)})),t.rejection=ui||Ai(t)?2:1,e.error))throw e.value}))},Ai=function(t){return 1!==t.rejection&&!t.parent},Ii=function(t){Zo.call(Jo,(function(){var e=t.facade;ui?wi.emit("rejectionHandled",e):Li("rejectionhandled",e,t.value)}))},Ri=function(t,e,n){return function(r){t(e,r,n)}},Ci=function(t,e,n){t.done||(t.done=!0,n&&(t=n),t.value=e,t.state=2,ki(t,!0))},Ni=function(t,e,n){if(!t.done){t.done=!0,n&&(t=n);try{if(t.facade===e)throw mi("Promise can't be resolved itself");var r=_i(e);r?ti((function(){var n={done:!1};try{r.call(e,Ri(Ni,n,t),Ri(Ci,n,t))}catch(e){Ci(n,e,t)}})):(t.value=e,t.state=1,ki(t,!1))}catch(e){Ci({done:!1},e,t)}}};if(Ti&&(gi=(yi=function(t){Qo(this,yi,li),Ko(t),_o.call(this);var e=pi(this);try{t(Ri(Ni,e),Ri(Ci,e))}catch(t){Ci(e,t)}}).prototype,(_o=function(t){hi(this,{type:li,done:!1,notified:!1,parent:!1,reactions:[],rejection:!1,state:0,value:void 0})}).prototype=Ho(gi,{then:function(t,e){var n=vi(this),r=ji(Vo(this,yi));return r.ok="function"!=typeof t||t,r.fail="function"==typeof e&&e,r.domain=ui?wi.domain:void 0,n.parent=!0,n.reactions.push(r),0!=n.state&&ki(n,!1),r.promise},catch:function(t){return this.then(void 0,t)}}),ko=function(){var t=new _o,e=pi(t);this.promise=t,this.resolve=Ri(Ni,e),this.reject=Ri(Ci,e)},ri.f=ji=function(t){return t===yi||t===Lo?new ko(t):xi(t)},"function"==typeof $o&&di!==Object.prototype)){Mo=di.then,Si||(Go(di,"then",(function(t,e){var n=this;return new yi((function(t,e){Mo.call(n,t,e)})).then(t,e)}),{unsafe:!0}),Go(di,"catch",gi.catch,{unsafe:!0}));try{delete di.constructor}catch(t){}zo&&zo(di,gi)}Fo({global:!0,wrap:!0,forced:Ti},{Promise:yi}),Wo(yi,li,!1),qo(li),Lo=Do(li),Fo({target:li,stat:!0,forced:Ti},{reject:function(t){var e=ji(this);return e.reject.call(void 0,t),e.promise}}),Fo({target:li,stat:!0,forced:Ti},{resolve:function(t){return ei(this,t)}}),Fo({target:li,stat:!0,forced:Pi},{all:function(t){var e=this,n=ji(e),r=n.resolve,o=n.reject,i=oi((function(){var n=Ko(e.resolve),i=[],a=0,c=1;Yo(t,(function(t){var u=a++,f=!1;i.push(void 0),c++,n.call(e,t).then((function(t){f||(f=!0,i[u]=t,--c||r(i))}),o)})),--c||r(i)}));return i.error&&o(i.value),n.promise},race:function(t){var e=this,n=ji(e),r=n.reject,o=oi((function(){var o=Ko(e.resolve);Yo(t,(function(t){o.call(e,t).then(n.resolve,r)}))}));return o.error&&r(o.value),n.promise}});var Fi,Ji=H,Di=W,$i=Ue,Gi=i?Object.defineProperties:function(t,e){Di(t);for(var n,r=$i(e),o=r.length,i=0;o>i;)Ji.f(t,n=r[i++],e[n]);return t},Hi=W,zi=Gi,Wi=ye,qi=Ot,Ui=Ir,Ki=M,Qi=xt("IE_PROTO"),Xi=function(){},Yi=function(t){return" + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +

    +
    + + +
    + +
    + + + + + reveal.js - Speaker View + + + + + + +
    Loading speaker view...
    + +
    +
    Upcoming
    +
    +
    +

    Time Click to Reset

    +
    + 0:00 AM +
    +
    + 00:00:00 +
    +
    + + + +
    + + +
    +
    + + +
    + + + +
    + +
    + +
    + +
    + + + + + + + + diff --git a/mstreams/presentation/plugin/search/plugin.js b/mstreams/presentation/plugin/search/plugin.js new file mode 100644 index 0000000..5d09ce6 --- /dev/null +++ b/mstreams/presentation/plugin/search/plugin.js @@ -0,0 +1,243 @@ +/*! + * Handles finding a text string anywhere in the slides and showing the next occurrence to the user + * by navigatating to that slide and highlighting it. + * + * @author Jon Snyder , February 2013 + */ + +const Plugin = () => { + + // The reveal.js instance this plugin is attached to + let deck; + + let searchElement; + let searchButton; + let searchInput; + + let matchedSlides; + let currentMatchedIndex; + let searchboxDirty; + let hilitor; + + function render() { + + searchElement = document.createElement( 'div' ); + searchElement.classList.add( 'searchbox' ); + searchElement.style.position = 'absolute'; + searchElement.style.top = '10px'; + searchElement.style.right = '10px'; + searchElement.style.zIndex = 10; + + //embedded base64 search icon Designed by Sketchdock - http://www.sketchdock.com/: + searchElement.innerHTML = ` + `; + + searchInput = searchElement.querySelector( '.searchinput' ); + searchInput.style.width = '240px'; + searchInput.style.fontSize = '14px'; + searchInput.style.padding = '4px 6px'; + searchInput.style.color = '#000'; + searchInput.style.background = '#fff'; + searchInput.style.borderRadius = '2px'; + searchInput.style.border = '0'; + searchInput.style.outline = '0'; + searchInput.style.boxShadow = '0 2px 18px rgba(0, 0, 0, 0.2)'; + searchInput.style['-webkit-appearance'] = 'none'; + + deck.getRevealElement().appendChild( searchElement ); + + // searchButton.addEventListener( 'click', function(event) { + // doSearch(); + // }, false ); + + searchInput.addEventListener( 'keyup', function( event ) { + switch (event.keyCode) { + case 13: + event.preventDefault(); + doSearch(); + searchboxDirty = false; + break; + default: + searchboxDirty = true; + } + }, false ); + + closeSearch(); + + } + + function openSearch() { + if( !searchElement ) render(); + + searchElement.style.display = 'inline'; + searchInput.focus(); + searchInput.select(); + } + + function closeSearch() { + if( !searchElement ) render(); + + searchElement.style.display = 'none'; + if(hilitor) hilitor.remove(); + } + + function toggleSearch() { + if( !searchElement ) render(); + + if (searchElement.style.display !== 'inline') { + openSearch(); + } + else { + closeSearch(); + } + } + + function doSearch() { + //if there's been a change in the search term, perform a new search: + if (searchboxDirty) { + var searchstring = searchInput.value; + + if (searchstring === '') { + if(hilitor) hilitor.remove(); + matchedSlides = null; + } + else { + //find the keyword amongst the slides + hilitor = new Hilitor("slidecontent"); + matchedSlides = hilitor.apply(searchstring); + currentMatchedIndex = 0; + } + } + + if (matchedSlides) { + //navigate to the next slide that has the keyword, wrapping to the first if necessary + if (matchedSlides.length && (matchedSlides.length <= currentMatchedIndex)) { + currentMatchedIndex = 0; + } + if (matchedSlides.length > currentMatchedIndex) { + deck.slide(matchedSlides[currentMatchedIndex].h, matchedSlides[currentMatchedIndex].v); + currentMatchedIndex++; + } + } + } + + // Original JavaScript code by Chirp Internet: www.chirp.com.au + // Please acknowledge use of this code by including this header. + // 2/2013 jon: modified regex to display any match, not restricted to word boundaries. + function Hilitor(id, tag) { + + var targetNode = document.getElementById(id) || document.body; + var hiliteTag = tag || "EM"; + var skipTags = new RegExp("^(?:" + hiliteTag + "|SCRIPT|FORM)$"); + var colors = ["#ff6", "#a0ffff", "#9f9", "#f99", "#f6f"]; + var wordColor = []; + var colorIdx = 0; + var matchRegex = ""; + var matchingSlides = []; + + this.setRegex = function(input) + { + input = input.replace(/^[^\w]+|[^\w]+$/g, "").replace(/[^\w'-]+/g, "|"); + matchRegex = new RegExp("(" + input + ")","i"); + } + + this.getRegex = function() + { + return matchRegex.toString().replace(/^\/\\b\(|\)\\b\/i$/g, "").replace(/\|/g, " "); + } + + // recursively apply word highlighting + this.hiliteWords = function(node) + { + if(node == undefined || !node) return; + if(!matchRegex) return; + if(skipTags.test(node.nodeName)) return; + + if(node.hasChildNodes()) { + for(var i=0; i < node.childNodes.length; i++) + this.hiliteWords(node.childNodes[i]); + } + if(node.nodeType == 3) { // NODE_TEXT + var nv, regs; + if((nv = node.nodeValue) && (regs = matchRegex.exec(nv))) { + //find the slide's section element and save it in our list of matching slides + var secnode = node; + while (secnode != null && secnode.nodeName != 'SECTION') { + secnode = secnode.parentNode; + } + + var slideIndex = deck.getIndices(secnode); + var slidelen = matchingSlides.length; + var alreadyAdded = false; + for (var i=0; i < slidelen; i++) { + if ( (matchingSlides[i].h === slideIndex.h) && (matchingSlides[i].v === slideIndex.v) ) { + alreadyAdded = true; + } + } + if (! alreadyAdded) { + matchingSlides.push(slideIndex); + } + + if(!wordColor[regs[0].toLowerCase()]) { + wordColor[regs[0].toLowerCase()] = colors[colorIdx++ % colors.length]; + } + + var match = document.createElement(hiliteTag); + match.appendChild(document.createTextNode(regs[0])); + match.style.backgroundColor = wordColor[regs[0].toLowerCase()]; + match.style.fontStyle = "inherit"; + match.style.color = "#000"; + + var after = node.splitText(regs.index); + after.nodeValue = after.nodeValue.substring(regs[0].length); + node.parentNode.insertBefore(match, after); + } + } + }; + + // remove highlighting + this.remove = function() + { + var arr = document.getElementsByTagName(hiliteTag); + var el; + while(arr.length && (el = arr[0])) { + el.parentNode.replaceChild(el.firstChild, el); + } + }; + + // start highlighting at target node + this.apply = function(input) + { + if(input == undefined || !input) return; + this.remove(); + this.setRegex(input); + this.hiliteWords(targetNode); + return matchingSlides; + }; + + } + + return { + + id: 'search', + + init: reveal => { + + deck = reveal; + deck.registerKeyboardShortcut( 'CTRL + Shift + F', 'Search' ); + + document.addEventListener( 'keydown', function( event ) { + if( event.key == "F" && (event.ctrlKey || event.metaKey) ) { //Control+Shift+f + event.preventDefault(); + toggleSearch(); + } + }, false ); + + }, + + open: openSearch + + } +}; + +export default Plugin; \ No newline at end of file diff --git a/mstreams/presentation/plugin/search/search.esm.js b/mstreams/presentation/plugin/search/search.esm.js new file mode 100644 index 0000000..b401a70 --- /dev/null +++ b/mstreams/presentation/plugin/search/search.esm.js @@ -0,0 +1,7 @@ +var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},t=function(e){try{return!!e()}catch(e){return!0}},n=!t((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]})),r=function(e){return e&&e.Math==Math&&e},o=r("object"==typeof globalThis&&globalThis)||r("object"==typeof window&&window)||r("object"==typeof self&&self)||r("object"==typeof e&&e)||function(){return this}()||Function("return this")(),i=t,c=/#|\.prototype\./,a=function(e,t){var n=l[u(e)];return n==s||n!=f&&("function"==typeof t?i(t):!!t)},u=a.normalize=function(e){return String(e).replace(c,".").toLowerCase()},l=a.data={},f=a.NATIVE="N",s=a.POLYFILL="P",p=a,g=function(e){return"object"==typeof e?null!==e:"function"==typeof e},d=g,h=function(e){if(!d(e))throw TypeError(String(e)+" is not an object");return e},y=g,v=h,x=function(e){if(!y(e)&&null!==e)throw TypeError("Can't set "+String(e)+" as a prototype");return e},b=Object.setPrototypeOf||("__proto__"in{}?function(){var e,t=!1,n={};try{(e=Object.getOwnPropertyDescriptor(Object.prototype,"__proto__").set).call(n,[]),t=n instanceof Array}catch(e){}return function(n,r){return v(n),x(r),t?e.call(n,r):n.__proto__=r,n}}():void 0),E=g,m=b,S={},w=g,O=o.document,R=w(O)&&w(O.createElement),T=function(e){return R?O.createElement(e):{}},_=!n&&!t((function(){return 7!=Object.defineProperty(T("div"),"a",{get:function(){return 7}}).a})),j=g,P=function(e,t){if(!j(e))return e;var n,r;if(t&&"function"==typeof(n=e.toString)&&!j(r=n.call(e)))return r;if("function"==typeof(n=e.valueOf)&&!j(r=n.call(e)))return r;if(!t&&"function"==typeof(n=e.toString)&&!j(r=n.call(e)))return r;throw TypeError("Can't convert object to primitive value")},I=n,C=_,N=h,A=P,k=Object.defineProperty;S.f=I?k:function(e,t,n){if(N(e),t=A(t,!0),N(n),C)try{return k(e,t,n)}catch(e){}if("get"in n||"set"in n)throw TypeError("Accessors not supported");return"value"in n&&(e[t]=n.value),e};var $={},L=function(e){if(null==e)throw TypeError("Can't call method on "+e);return e},M=L,U=function(e){return Object(M(e))},D=U,F={}.hasOwnProperty,z=function(e,t){return F.call(D(e),t)},K={}.toString,B=function(e){return K.call(e).slice(8,-1)},W=B,G="".split,V=t((function(){return!Object("z").propertyIsEnumerable(0)}))?function(e){return"String"==W(e)?G.call(e,""):Object(e)}:Object,Y=L,q=function(e){return V(Y(e))},X=Math.ceil,H=Math.floor,J=function(e){return isNaN(e=+e)?0:(e>0?H:X)(e)},Q=J,Z=Math.min,ee=function(e){return e>0?Z(Q(e),9007199254740991):0},te=J,ne=Math.max,re=Math.min,oe=q,ie=ee,ce=function(e,t){var n=te(e);return n<0?ne(n+t,0):re(n,t)},ae=function(e){return function(t,n,r){var o,i=oe(t),c=ie(i.length),a=ce(r,c);if(e&&n!=n){for(;c>a;)if((o=i[a++])!=o)return!0}else for(;c>a;a++)if((e||a in i)&&i[a]===n)return e||a||0;return!e&&-1}},ue={includes:ae(!0),indexOf:ae(!1)},le={},fe=z,se=q,pe=ue.indexOf,ge=le,de=function(e,t){var n,r=se(e),o=0,i=[];for(n in r)!fe(ge,n)&&fe(r,n)&&i.push(n);for(;t.length>o;)fe(r,n=t[o++])&&(~pe(i,n)||i.push(n));return i},he=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"].concat("length","prototype");$.f=Object.getOwnPropertyNames||function(e){return de(e,he)};var ye={exports:{}},ve=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}},xe=S,be=ve,Ee=n?function(e,t,n){return xe.f(e,t,be(1,n))}:function(e,t,n){return e[t]=n,e},me=o,Se=Ee,we=function(e,t){try{Se(me,e,t)}catch(n){me[e]=t}return t},Oe=we,Re=o["__core-js_shared__"]||Oe("__core-js_shared__",{}),Te=Re;(ye.exports=function(e,t){return Te[e]||(Te[e]=void 0!==t?t:{})})("versions",[]).push({version:"3.12.1",mode:"global",copyright:"© 2021 Denis Pushkarev (zloirock.ru)"});var _e,je,Pe=0,Ie=Math.random(),Ce=function(e){return"Symbol("+String(void 0===e?"":e)+")_"+(++Pe+Ie).toString(36)},Ne=o,Ae=o,ke=function(e){return"function"==typeof e?e:void 0},$e=function(e,t){return arguments.length<2?ke(Ne[e])||ke(Ae[e]):Ne[e]&&Ne[e][t]||Ae[e]&&Ae[e][t]},Le=$e("navigator","userAgent")||"",Me=o.process,Ue=Me&&Me.versions,De=Ue&&Ue.v8;De?je=(_e=De.split("."))[0]<4?1:_e[0]+_e[1]:Le&&(!(_e=Le.match(/Edge\/(\d+)/))||_e[1]>=74)&&(_e=Le.match(/Chrome\/(\d+)/))&&(je=_e[1]);var Fe=je&&+je,ze=t,Ke=!!Object.getOwnPropertySymbols&&!ze((function(){return!String(Symbol())||!Symbol.sham&&Fe&&Fe<41})),Be=Ke&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,We=o,Ge=ye.exports,Ve=z,Ye=Ce,qe=Ke,Xe=Be,He=Ge("wks"),Je=We.Symbol,Qe=Xe?Je:Je&&Je.withoutSetter||Ye,Ze=function(e){return Ve(He,e)&&(qe||"string"==typeof He[e])||(qe&&Ve(Je,e)?He[e]=Je[e]:He[e]=Qe("Symbol."+e)),He[e]},et=g,tt=B,nt=Ze("match"),rt=h,ot=function(){var e=rt(this),t="";return e.global&&(t+="g"),e.ignoreCase&&(t+="i"),e.multiline&&(t+="m"),e.dotAll&&(t+="s"),e.unicode&&(t+="u"),e.sticky&&(t+="y"),t},it={},ct=t;function at(e,t){return RegExp(e,t)}it.UNSUPPORTED_Y=ct((function(){var e=at("a","y");return e.lastIndex=2,null!=e.exec("abcd")})),it.BROKEN_CARET=ct((function(){var e=at("^r","gy");return e.lastIndex=2,null!=e.exec("str")}));var ut={exports:{}},lt=Re,ft=Function.toString;"function"!=typeof lt.inspectSource&&(lt.inspectSource=function(e){return ft.call(e)});var st,pt,gt,dt=lt.inspectSource,ht=dt,yt=o.WeakMap,vt="function"==typeof yt&&/native code/.test(ht(yt)),xt=ye.exports,bt=Ce,Et=xt("keys"),mt=vt,St=g,wt=Ee,Ot=z,Rt=Re,Tt=function(e){return Et[e]||(Et[e]=bt(e))},_t=le,jt=o.WeakMap;if(mt||Rt.state){var Pt=Rt.state||(Rt.state=new jt),It=Pt.get,Ct=Pt.has,Nt=Pt.set;st=function(e,t){if(Ct.call(Pt,e))throw new TypeError("Object already initialized");return t.facade=e,Nt.call(Pt,e,t),t},pt=function(e){return It.call(Pt,e)||{}},gt=function(e){return Ct.call(Pt,e)}}else{var At=Tt("state");_t[At]=!0,st=function(e,t){if(Ot(e,At))throw new TypeError("Object already initialized");return t.facade=e,wt(e,At,t),t},pt=function(e){return Ot(e,At)?e[At]:{}},gt=function(e){return Ot(e,At)}}var kt={set:st,get:pt,has:gt,enforce:function(e){return gt(e)?pt(e):st(e,{})},getterFor:function(e){return function(t){var n;if(!St(t)||(n=pt(t)).type!==e)throw TypeError("Incompatible receiver, "+e+" required");return n}}},$t=o,Lt=Ee,Mt=z,Ut=we,Dt=dt,Ft=kt.get,zt=kt.enforce,Kt=String(String).split("String");(ut.exports=function(e,t,n,r){var o,i=!!r&&!!r.unsafe,c=!!r&&!!r.enumerable,a=!!r&&!!r.noTargetGet;"function"==typeof n&&("string"!=typeof t||Mt(n,"name")||Lt(n,"name",t),(o=zt(n)).source||(o.source=Kt.join("string"==typeof t?t:""))),e!==$t?(i?!a&&e[t]&&(c=!0):delete e[t],c?e[t]=n:Lt(e,t,n)):c?e[t]=n:Ut(t,n)})(Function.prototype,"toString",(function(){return"function"==typeof this&&Ft(this).source||Dt(this)}));var Bt=$e,Wt=S,Gt=n,Vt=Ze("species"),Yt=n,qt=o,Xt=p,Ht=function(e,t,n){var r,o;return m&&"function"==typeof(r=t.constructor)&&r!==n&&E(o=r.prototype)&&o!==n.prototype&&m(e,o),e},Jt=S.f,Qt=$.f,Zt=function(e){var t;return et(e)&&(void 0!==(t=e[nt])?!!t:"RegExp"==tt(e))},en=ot,tn=it,nn=ut.exports,rn=t,on=kt.enforce,cn=function(e){var t=Bt(e),n=Wt.f;Gt&&t&&!t[Vt]&&n(t,Vt,{configurable:!0,get:function(){return this}})},an=Ze("match"),un=qt.RegExp,ln=un.prototype,fn=/a/g,sn=/a/g,pn=new un(fn)!==fn,gn=tn.UNSUPPORTED_Y;if(Yt&&Xt("RegExp",!pn||gn||rn((function(){return sn[an]=!1,un(fn)!=fn||un(sn)==sn||"/a/i"!=un(fn,"i")})))){for(var dn=function(e,t){var n,r=this instanceof dn,o=Zt(e),i=void 0===t;if(!r&&o&&e.constructor===dn&&i)return e;pn?o&&!i&&(e=e.source):e instanceof dn&&(i&&(t=en.call(e)),e=e.source),gn&&(n=!!t&&t.indexOf("y")>-1)&&(t=t.replace(/y/g,""));var c=Ht(pn?new un(e,t):un(e,t),r?this:ln,dn);gn&&n&&(on(c).sticky=!0);return c},hn=function(e){e in dn||Jt(dn,e,{configurable:!0,get:function(){return un[e]},set:function(t){un[e]=t}})},yn=Qt(un),vn=0;yn.length>vn;)hn(yn[vn++]);ln.constructor=dn,dn.prototype=ln,nn(qt,"RegExp",dn)}cn("RegExp");var xn={},bn={},En={}.propertyIsEnumerable,mn=Object.getOwnPropertyDescriptor,Sn=mn&&!En.call({1:2},1);bn.f=Sn?function(e){var t=mn(this,e);return!!t&&t.enumerable}:En;var wn=n,On=bn,Rn=ve,Tn=q,_n=P,jn=z,Pn=_,In=Object.getOwnPropertyDescriptor;xn.f=wn?In:function(e,t){if(e=Tn(e),t=_n(t,!0),Pn)try{return In(e,t)}catch(e){}if(jn(e,t))return Rn(!On.f.call(e,t),e[t])};var Cn={};Cn.f=Object.getOwnPropertySymbols;var Nn=$,An=Cn,kn=h,$n=$e("Reflect","ownKeys")||function(e){var t=Nn.f(kn(e)),n=An.f;return n?t.concat(n(e)):t},Ln=z,Mn=$n,Un=xn,Dn=S,Fn=o,zn=xn.f,Kn=Ee,Bn=ut.exports,Wn=we,Gn=function(e,t){for(var n=Mn(t),r=Dn.f,o=Un.f,i=0;i0&&(!i.multiline||i.multiline&&"\n"!==e[i.lastIndex-1])&&(u="(?: "+u+")",f=" "+f,l++),n=new RegExp("^(?:"+u+")",a)),tr&&(n=new RegExp("^"+u+"$(?!\\s)",a)),Zn&&(t=i.lastIndex),r=Hn.call(c?n:i,f),c?r?(r.input=r.input.slice(l),r[0]=r[0].slice(l),r.index=i.lastIndex,i.lastIndex+=r[0].length):i.lastIndex=0:Zn&&r&&(i.lastIndex=i.global?r.index+r[0].length:t),tr&&r&&r.length>1&&Jn.call(r[0],n,(function(){for(o=1;o")})),br="$0"==="a".replace(/./,"$0"),Er=dr("replace"),mr=!!/./[Er]&&""===/./[Er]("a","$0"),Sr=!gr((function(){var e=/(?:)/,t=e.exec;e.exec=function(){return t.apply(this,arguments)};var n="ab".split(e);return 2!==n.length||"a"!==n[0]||"b"!==n[1]})),wr=J,Or=L,Rr=function(e){return function(t,n){var r,o,i=String(Or(t)),c=wr(n),a=i.length;return c<0||c>=a?e?"":void 0:(r=i.charCodeAt(c))<55296||r>56319||c+1===a||(o=i.charCodeAt(c+1))<56320||o>57343?e?i.charAt(c):r:e?i.slice(c,c+2):o-56320+(r-55296<<10)+65536}},Tr={codeAt:Rr(!1),charAt:Rr(!0)}.charAt,_r=U,jr=Math.floor,Pr="".replace,Ir=/\$([$&'`]|\d{1,2}|<[^>]*>)/g,Cr=/\$([$&'`]|\d{1,2})/g,Nr=B,Ar=nr,kr=function(e,t,n,r){var o=dr(e),i=!gr((function(){var t={};return t[o]=function(){return 7},7!=""[e](t)})),c=i&&!gr((function(){var t=!1,n=/a/;return"split"===e&&((n={}).constructor={},n.constructor[yr]=function(){return n},n.flags="",n[o]=/./[o]),n.exec=function(){return t=!0,null},n[o](""),!t}));if(!i||!c||"replace"===e&&(!xr||!br||mr)||"split"===e&&!Sr){var a=/./[o],u=n(o,""[e],(function(e,t,n,r,o){var c=t.exec;return c===pr||c===vr.exec?i&&!o?{done:!0,value:a.call(t,n,r)}:{done:!0,value:e.call(n,t,r)}:{done:!1}}),{REPLACE_KEEPS_$0:br,REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE:mr}),l=u[0],f=u[1];sr(String.prototype,e,l),sr(vr,o,2==t?function(e,t){return f.call(e,this,t)}:function(e){return f.call(e,this)})}r&&hr(vr[o],"sham",!0)},$r=h,Lr=ee,Mr=J,Ur=L,Dr=function(e,t,n){return t+(n?Tr(e,t).length:1)},Fr=function(e,t,n,r,o,i){var c=n+e.length,a=r.length,u=Cr;return void 0!==o&&(o=_r(o),u=Ir),Pr.call(i,u,(function(i,u){var l;switch(u.charAt(0)){case"$":return"$";case"&":return e;case"`":return t.slice(0,n);case"'":return t.slice(c);case"<":l=o[u.slice(1,-1)];break;default:var f=+u;if(0===f)return i;if(f>a){var s=jr(f/10);return 0===s?i:s<=a?void 0===r[s-1]?u.charAt(1):r[s-1]+u.charAt(1):i}l=r[f-1]}return void 0===l?"":l}))},zr=function(e,t){var n=e.exec;if("function"==typeof n){var r=n.call(e,t);if("object"!=typeof r)throw TypeError("RegExp exec method returned something other than an Object or null");return r}if("RegExp"!==Nr(e))throw TypeError("RegExp#exec called on incompatible receiver");return Ar.call(e,t)},Kr=Math.max,Br=Math.min;kr("replace",2,(function(e,t,n,r){var o=r.REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE,i=r.REPLACE_KEEPS_$0,c=o?"$":"$0";return[function(n,r){var o=Ur(this),i=null==n?void 0:n[e];return void 0!==i?i.call(n,o,r):t.call(String(o),n,r)},function(e,r){if(!o&&i||"string"==typeof r&&-1===r.indexOf(c)){var a=n(t,e,this,r);if(a.done)return a.value}var u=$r(e),l=String(this),f="function"==typeof r;f||(r=String(r));var s=u.global;if(s){var p=u.unicode;u.lastIndex=0}for(var g=[];;){var d=zr(u,l);if(null===d)break;if(g.push(d),!s)break;""===String(d[0])&&(u.lastIndex=Dr(l,Lr(u.lastIndex),p))}for(var h,y="",v=0,x=0;x=v&&(y+=l.slice(v,E)+R,v=E+b.length)}return y+l.slice(v)}]}));var Wr={};Wr[Ze("toStringTag")]="z";var Gr="[object z]"===String(Wr),Vr=Gr,Yr=B,qr=Ze("toStringTag"),Xr="Arguments"==Yr(function(){return arguments}()),Hr=Vr?Yr:function(e){var t,n,r;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(n=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),qr))?n:Xr?Yr(t):"Object"==(r=Yr(t))&&"function"==typeof t.callee?"Arguments":r},Jr=Gr?{}.toString:function(){return"[object "+Hr(this)+"]"},Qr=Gr,Zr=ut.exports,eo=Jr;Qr||Zr(Object.prototype,"toString",eo,{unsafe:!0}) +/*! + * Handles finding a text string anywhere in the slides and showing the next occurrence to the user + * by navigatating to that slide and highlighting it. + * + * @author Jon Snyder , February 2013 + */;export default function(){var e,t,n,r,o,i,c;function a(){(t=document.createElement("div")).classList.add("searchbox"),t.style.position="absolute",t.style.top="10px",t.style.right="10px",t.style.zIndex=10,t.innerHTML='\n\t\t',(n=t.querySelector(".searchinput")).style.width="240px",n.style.fontSize="14px",n.style.padding="4px 6px",n.style.color="#000",n.style.background="#fff",n.style.borderRadius="2px",n.style.border="0",n.style.outline="0",n.style.boxShadow="0 2px 18px rgba(0, 0, 0, 0.2)",n.style["-webkit-appearance"]="none",e.getRevealElement().appendChild(t),n.addEventListener("keyup",(function(t){switch(t.keyCode){case 13:t.preventDefault(),function(){if(i){var t=n.value;""===t?(c&&c.remove(),r=null):(c=new f("slidecontent"),r=c.apply(t),o=0)}r&&(r.length&&r.length<=o&&(o=0),r.length>o&&(e.slide(r[o].h,r[o].v),o++))}(),i=!1;break;default:i=!0}}),!1),l()}function u(){t||a(),t.style.display="inline",n.focus(),n.select()}function l(){t||a(),t.style.display="none",c&&c.remove()}function f(t,n){var r=document.getElementById(t)||document.body,o=n||"EM",i=new RegExp("^(?:"+o+"|SCRIPT|FORM)$"),c=["#ff6","#a0ffff","#9f9","#f99","#f6f"],a=[],u=0,l="",f=[];this.setRegex=function(e){e=e.replace(/^[^\w]+|[^\w]+$/g,"").replace(/[^\w'-]+/g,"|"),l=new RegExp("("+e+")","i")},this.getRegex=function(){return l.toString().replace(/^\/\\b\(|\)\\b\/i$/g,"").replace(/\|/g," ")},this.hiliteWords=function(t){if(null!=t&&t&&l&&!i.test(t.nodeName)){if(t.hasChildNodes())for(var n=0;n0?H:X)(e)},Q=J,Z=Math.min,ee=function(e){return e>0?Z(Q(e),9007199254740991):0},te=J,ne=Math.max,re=Math.min,oe=q,ie=ee,ce=function(e,t){var n=te(e);return n<0?ne(n+t,0):re(n,t)},ae=function(e){return function(t,n,r){var o,i=oe(t),c=ie(i.length),a=ce(r,c);if(e&&n!=n){for(;c>a;)if((o=i[a++])!=o)return!0}else for(;c>a;a++)if((e||a in i)&&i[a]===n)return e||a||0;return!e&&-1}},ue={includes:ae(!0),indexOf:ae(!1)},le={},fe=z,se=q,pe=ue.indexOf,de=le,ge=function(e,t){var n,r=se(e),o=0,i=[];for(n in r)!fe(de,n)&&fe(r,n)&&i.push(n);for(;t.length>o;)fe(r,n=t[o++])&&(~pe(i,n)||i.push(n));return i},he=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"].concat("length","prototype");$.f=Object.getOwnPropertyNames||function(e){return ge(e,he)};var ye={exports:{}},ve=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}},xe=S,be=ve,me=n?function(e,t,n){return xe.f(e,t,be(1,n))}:function(e,t,n){return e[t]=n,e},Ee=o,Se=me,we=function(e,t){try{Se(Ee,e,t)}catch(n){Ee[e]=t}return t},Oe=we,Re="__core-js_shared__",Te=o[Re]||Oe(Re,{}),_e=Te;(ye.exports=function(e,t){return _e[e]||(_e[e]=void 0!==t?t:{})})("versions",[]).push({version:"3.12.1",mode:"global",copyright:"© 2021 Denis Pushkarev (zloirock.ru)"});var je,Pe,Ie=0,Ce=Math.random(),Ne=function(e){return"Symbol("+String(void 0===e?"":e)+")_"+(++Ie+Ce).toString(36)},Ae=o,ke=o,$e=function(e){return"function"==typeof e?e:void 0},Le=function(e,t){return arguments.length<2?$e(Ae[e])||$e(ke[e]):Ae[e]&&Ae[e][t]||ke[e]&&ke[e][t]},Me=Le("navigator","userAgent")||"",Ue=o.process,De=Ue&&Ue.versions,Fe=De&&De.v8;Fe?Pe=(je=Fe.split("."))[0]<4?1:je[0]+je[1]:Me&&(!(je=Me.match(/Edge\/(\d+)/))||je[1]>=74)&&(je=Me.match(/Chrome\/(\d+)/))&&(Pe=je[1]);var ze=Pe&&+Pe,Ke=t,Be=!!Object.getOwnPropertySymbols&&!Ke((function(){return!String(Symbol())||!Symbol.sham&&ze&&ze<41})),We=Be&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,Ge=o,Ve=ye.exports,Ye=z,qe=Ne,Xe=Be,He=We,Je=Ve("wks"),Qe=Ge.Symbol,Ze=He?Qe:Qe&&Qe.withoutSetter||qe,et=function(e){return Ye(Je,e)&&(Xe||"string"==typeof Je[e])||(Xe&&Ye(Qe,e)?Je[e]=Qe[e]:Je[e]=Ze("Symbol."+e)),Je[e]},tt=d,nt=B,rt=et("match"),ot=h,it=function(){var e=ot(this),t="";return e.global&&(t+="g"),e.ignoreCase&&(t+="i"),e.multiline&&(t+="m"),e.dotAll&&(t+="s"),e.unicode&&(t+="u"),e.sticky&&(t+="y"),t},ct={},at=t;function ut(e,t){return RegExp(e,t)}ct.UNSUPPORTED_Y=at((function(){var e=ut("a","y");return e.lastIndex=2,null!=e.exec("abcd")})),ct.BROKEN_CARET=at((function(){var e=ut("^r","gy");return e.lastIndex=2,null!=e.exec("str")}));var lt={exports:{}},ft=Te,st=Function.toString;"function"!=typeof ft.inspectSource&&(ft.inspectSource=function(e){return st.call(e)});var pt,dt,gt,ht=ft.inspectSource,yt=ht,vt=o.WeakMap,xt="function"==typeof vt&&/native code/.test(yt(vt)),bt=ye.exports,mt=Ne,Et=bt("keys"),St=xt,wt=d,Ot=me,Rt=z,Tt=Te,_t=function(e){return Et[e]||(Et[e]=mt(e))},jt=le,Pt="Object already initialized",It=o.WeakMap;if(St||Tt.state){var Ct=Tt.state||(Tt.state=new It),Nt=Ct.get,At=Ct.has,kt=Ct.set;pt=function(e,t){if(At.call(Ct,e))throw new TypeError(Pt);return t.facade=e,kt.call(Ct,e,t),t},dt=function(e){return Nt.call(Ct,e)||{}},gt=function(e){return At.call(Ct,e)}}else{var $t=_t("state");jt[$t]=!0,pt=function(e,t){if(Rt(e,$t))throw new TypeError(Pt);return t.facade=e,Ot(e,$t,t),t},dt=function(e){return Rt(e,$t)?e[$t]:{}},gt=function(e){return Rt(e,$t)}}var Lt={set:pt,get:dt,has:gt,enforce:function(e){return gt(e)?dt(e):pt(e,{})},getterFor:function(e){return function(t){var n;if(!wt(t)||(n=dt(t)).type!==e)throw TypeError("Incompatible receiver, "+e+" required");return n}}},Mt=o,Ut=me,Dt=z,Ft=we,zt=ht,Kt=Lt.get,Bt=Lt.enforce,Wt=String(String).split("String");(lt.exports=function(e,t,n,r){var o,i=!!r&&!!r.unsafe,c=!!r&&!!r.enumerable,a=!!r&&!!r.noTargetGet;"function"==typeof n&&("string"!=typeof t||Dt(n,"name")||Ut(n,"name",t),(o=Bt(n)).source||(o.source=Wt.join("string"==typeof t?t:""))),e!==Mt?(i?!a&&e[t]&&(c=!0):delete e[t],c?e[t]=n:Ut(e,t,n)):c?e[t]=n:Ft(t,n)})(Function.prototype,"toString",(function(){return"function"==typeof this&&Kt(this).source||zt(this)}));var Gt=Le,Vt=S,Yt=n,qt=et("species"),Xt=n,Ht=o,Jt=p,Qt=function(e,t,n){var r,o;return E&&"function"==typeof(r=t.constructor)&&r!==n&&m(o=r.prototype)&&o!==n.prototype&&E(e,o),e},Zt=S.f,en=$.f,tn=function(e){var t;return tt(e)&&(void 0!==(t=e[rt])?!!t:"RegExp"==nt(e))},nn=it,rn=ct,on=lt.exports,cn=t,an=Lt.enforce,un=function(e){var t=Gt(e),n=Vt.f;Yt&&t&&!t[qt]&&n(t,qt,{configurable:!0,get:function(){return this}})},ln=et("match"),fn=Ht.RegExp,sn=fn.prototype,pn=/a/g,dn=/a/g,gn=new fn(pn)!==pn,hn=rn.UNSUPPORTED_Y;if(Xt&&Jt("RegExp",!gn||hn||cn((function(){return dn[ln]=!1,fn(pn)!=pn||fn(dn)==dn||"/a/i"!=fn(pn,"i")})))){for(var yn=function(e,t){var n,r=this instanceof yn,o=tn(e),i=void 0===t;if(!r&&o&&e.constructor===yn&&i)return e;gn?o&&!i&&(e=e.source):e instanceof yn&&(i&&(t=nn.call(e)),e=e.source),hn&&(n=!!t&&t.indexOf("y")>-1)&&(t=t.replace(/y/g,""));var c=Qt(gn?new fn(e,t):fn(e,t),r?this:sn,yn);hn&&n&&(an(c).sticky=!0);return c},vn=function(e){e in yn||Zt(yn,e,{configurable:!0,get:function(){return fn[e]},set:function(t){fn[e]=t}})},xn=en(fn),bn=0;xn.length>bn;)vn(xn[bn++]);sn.constructor=yn,yn.prototype=sn,on(Ht,"RegExp",yn)}un("RegExp");var mn={},En={},Sn={}.propertyIsEnumerable,wn=Object.getOwnPropertyDescriptor,On=wn&&!Sn.call({1:2},1);En.f=On?function(e){var t=wn(this,e);return!!t&&t.enumerable}:Sn;var Rn=n,Tn=En,_n=ve,jn=q,Pn=P,In=z,Cn=_,Nn=Object.getOwnPropertyDescriptor;mn.f=Rn?Nn:function(e,t){if(e=jn(e),t=Pn(t,!0),Cn)try{return Nn(e,t)}catch(e){}if(In(e,t))return _n(!Tn.f.call(e,t),e[t])};var An={};An.f=Object.getOwnPropertySymbols;var kn=$,$n=An,Ln=h,Mn=Le("Reflect","ownKeys")||function(e){var t=kn.f(Ln(e)),n=$n.f;return n?t.concat(n(e)):t},Un=z,Dn=Mn,Fn=mn,zn=S,Kn=o,Bn=mn.f,Wn=me,Gn=lt.exports,Vn=we,Yn=function(e,t){for(var n=Dn(t),r=zn.f,o=Fn.f,i=0;i0&&(!i.multiline||i.multiline&&"\n"!==e[i.lastIndex-1])&&(u="(?: "+u+")",f=" "+f,l++),n=new RegExp("^(?:"+u+")",a)),rr&&(n=new RegExp("^"+u+"$(?!\\s)",a)),tr&&(t=i.lastIndex),r=Qn.call(c?n:i,f),c?r?(r.input=r.input.slice(l),r[0]=r[0].slice(l),r.index=i.lastIndex,i.lastIndex+=r[0].length):i.lastIndex=0:tr&&r&&(i.lastIndex=i.global?r.index+r[0].length:t),rr&&r&&r.length>1&&Zn.call(r[0],n,(function(){for(o=1;o")})),Sr="$0"==="a".replace(/./,"$0"),wr=vr("replace"),Or=!!/./[wr]&&""===/./[wr]("a","$0"),Rr=!yr((function(){var e=/(?:)/,t=e.exec;e.exec=function(){return t.apply(this,arguments)};var n="ab".split(e);return 2!==n.length||"a"!==n[0]||"b"!==n[1]})),Tr=J,_r=L,jr=function(e){return function(t,n){var r,o,i=String(_r(t)),c=Tr(n),a=i.length;return c<0||c>=a?e?"":void 0:(r=i.charCodeAt(c))<55296||r>56319||c+1===a||(o=i.charCodeAt(c+1))<56320||o>57343?e?i.charAt(c):r:e?i.slice(c,c+2):o-56320+(r-55296<<10)+65536}},Pr={codeAt:jr(!1),charAt:jr(!0)}.charAt,Ir=U,Cr=Math.floor,Nr="".replace,Ar=/\$([$&'`]|\d{1,2}|<[^>]*>)/g,kr=/\$([$&'`]|\d{1,2})/g,$r=B,Lr=or,Mr=function(e,t,n,r){var o=vr(e),i=!yr((function(){var t={};return t[o]=function(){return 7},7!=""[e](t)})),c=i&&!yr((function(){var t=!1,n=/a/;return"split"===e&&((n={}).constructor={},n.constructor[br]=function(){return n},n.flags="",n[o]=/./[o]),n.exec=function(){return t=!0,null},n[o](""),!t}));if(!i||!c||"replace"===e&&(!Er||!Sr||Or)||"split"===e&&!Rr){var a=/./[o],u=n(o,""[e],(function(e,t,n,r,o){var c=t.exec;return c===hr||c===mr.exec?i&&!o?{done:!0,value:a.call(t,n,r)}:{done:!0,value:e.call(n,t,r)}:{done:!1}}),{REPLACE_KEEPS_$0:Sr,REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE:Or}),l=u[0],f=u[1];gr(String.prototype,e,l),gr(mr,o,2==t?function(e,t){return f.call(e,this,t)}:function(e){return f.call(e,this)})}r&&xr(mr[o],"sham",!0)},Ur=h,Dr=ee,Fr=J,zr=L,Kr=function(e,t,n){return t+(n?Pr(e,t).length:1)},Br=function(e,t,n,r,o,i){var c=n+e.length,a=r.length,u=kr;return void 0!==o&&(o=Ir(o),u=Ar),Nr.call(i,u,(function(i,u){var l;switch(u.charAt(0)){case"$":return"$";case"&":return e;case"`":return t.slice(0,n);case"'":return t.slice(c);case"<":l=o[u.slice(1,-1)];break;default:var f=+u;if(0===f)return i;if(f>a){var s=Cr(f/10);return 0===s?i:s<=a?void 0===r[s-1]?u.charAt(1):r[s-1]+u.charAt(1):i}l=r[f-1]}return void 0===l?"":l}))},Wr=function(e,t){var n=e.exec;if("function"==typeof n){var r=n.call(e,t);if("object"!=typeof r)throw TypeError("RegExp exec method returned something other than an Object or null");return r}if("RegExp"!==$r(e))throw TypeError("RegExp#exec called on incompatible receiver");return Lr.call(e,t)},Gr=Math.max,Vr=Math.min;Mr("replace",2,(function(e,t,n,r){var o=r.REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE,i=r.REPLACE_KEEPS_$0,c=o?"$":"$0";return[function(n,r){var o=zr(this),i=null==n?void 0:n[e];return void 0!==i?i.call(n,o,r):t.call(String(o),n,r)},function(e,r){if(!o&&i||"string"==typeof r&&-1===r.indexOf(c)){var a=n(t,e,this,r);if(a.done)return a.value}var u=Ur(e),l=String(this),f="function"==typeof r;f||(r=String(r));var s=u.global;if(s){var p=u.unicode;u.lastIndex=0}for(var d=[];;){var g=Wr(u,l);if(null===g)break;if(d.push(g),!s)break;""===String(g[0])&&(u.lastIndex=Kr(l,Dr(u.lastIndex),p))}for(var h,y="",v=0,x=0;x=v&&(y+=l.slice(v,m)+R,v=m+b.length)}return y+l.slice(v)}]}));var Yr={};Yr[et("toStringTag")]="z";var qr="[object z]"===String(Yr),Xr=qr,Hr=B,Jr=et("toStringTag"),Qr="Arguments"==Hr(function(){return arguments}()),Zr=Xr?Hr:function(e){var t,n,r;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(n=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),Jr))?n:Qr?Hr(t):"Object"==(r=Hr(t))&&"function"==typeof t.callee?"Arguments":r},eo=qr?{}.toString:function(){return"[object "+Zr(this)+"]"},to=qr,no=lt.exports,ro=eo;to||no(Object.prototype,"toString",ro,{unsafe:!0}) +/*! + * Handles finding a text string anywhere in the slides and showing the next occurrence to the user + * by navigatating to that slide and highlighting it. + * + * @author Jon Snyder , February 2013 + */;return function(){var e,t,n,r,o,i,c;function a(){(t=document.createElement("div")).classList.add("searchbox"),t.style.position="absolute",t.style.top="10px",t.style.right="10px",t.style.zIndex=10,t.innerHTML='\n\t\t',(n=t.querySelector(".searchinput")).style.width="240px",n.style.fontSize="14px",n.style.padding="4px 6px",n.style.color="#000",n.style.background="#fff",n.style.borderRadius="2px",n.style.border="0",n.style.outline="0",n.style.boxShadow="0 2px 18px rgba(0, 0, 0, 0.2)",n.style["-webkit-appearance"]="none",e.getRevealElement().appendChild(t),n.addEventListener("keyup",(function(t){switch(t.keyCode){case 13:t.preventDefault(),function(){if(i){var t=n.value;""===t?(c&&c.remove(),r=null):(c=new f("slidecontent"),r=c.apply(t),o=0)}r&&(r.length&&r.length<=o&&(o=0),r.length>o&&(e.slide(r[o].h,r[o].v),o++))}(),i=!1;break;default:i=!0}}),!1),l()}function u(){t||a(),t.style.display="inline",n.focus(),n.select()}function l(){t||a(),t.style.display="none",c&&c.remove()}function f(t,n){var r=document.getElementById(t)||document.body,o=n||"EM",i=new RegExp("^(?:"+o+"|SCRIPT|FORM)$"),c=["#ff6","#a0ffff","#9f9","#f99","#f6f"],a=[],u=0,l="",f=[];this.setRegex=function(e){e=e.replace(/^[^\w]+|[^\w]+$/g,"").replace(/[^\w'-]+/g,"|"),l=new RegExp("("+e+")","i")},this.getRegex=function(){return l.toString().replace(/^\/\\b\(|\)\\b\/i$/g,"").replace(/\|/g," ")},this.hiliteWords=function(t){if(null!=t&&t&&l&&!i.test(t.nodeName)){if(t.hasChildNodes())for(var n=0;n { + + zoom.reset(); + + } + +}; + +export default () => Plugin; + +/*! + * zoom.js 0.3 (modified for use with reveal.js) + * http://lab.hakim.se/zoom-js + * MIT licensed + * + * Copyright (C) 2011-2014 Hakim El Hattab, http://hakim.se + */ +var zoom = (function(){ + + // The current zoom level (scale) + var level = 1; + + // The current mouse position, used for panning + var mouseX = 0, + mouseY = 0; + + // Timeout before pan is activated + var panEngageTimeout = -1, + panUpdateInterval = -1; + + // Check for transform support so that we can fallback otherwise + var supportsTransforms = 'transform' in document.body.style; + + if( supportsTransforms ) { + // The easing that will be applied when we zoom in/out + document.body.style.transition = 'transform 0.8s ease'; + } + + // Zoom out if the user hits escape + document.addEventListener( 'keyup', function( event ) { + if( level !== 1 && event.keyCode === 27 ) { + zoom.out(); + } + } ); + + // Monitor mouse movement for panning + document.addEventListener( 'mousemove', function( event ) { + if( level !== 1 ) { + mouseX = event.clientX; + mouseY = event.clientY; + } + } ); + + /** + * Applies the CSS required to zoom in, prefers the use of CSS3 + * transforms but falls back on zoom for IE. + * + * @param {Object} rect + * @param {Number} scale + */ + function magnify( rect, scale ) { + + var scrollOffset = getScrollOffset(); + + // Ensure a width/height is set + rect.width = rect.width || 1; + rect.height = rect.height || 1; + + // Center the rect within the zoomed viewport + rect.x -= ( window.innerWidth - ( rect.width * scale ) ) / 2; + rect.y -= ( window.innerHeight - ( rect.height * scale ) ) / 2; + + if( supportsTransforms ) { + // Reset + if( scale === 1 ) { + document.body.style.transform = ''; + } + // Scale + else { + var origin = scrollOffset.x +'px '+ scrollOffset.y +'px', + transform = 'translate('+ -rect.x +'px,'+ -rect.y +'px) scale('+ scale +')'; + + document.body.style.transformOrigin = origin; + document.body.style.transform = transform; + } + } + else { + // Reset + if( scale === 1 ) { + document.body.style.position = ''; + document.body.style.left = ''; + document.body.style.top = ''; + document.body.style.width = ''; + document.body.style.height = ''; + document.body.style.zoom = ''; + } + // Scale + else { + document.body.style.position = 'relative'; + document.body.style.left = ( - ( scrollOffset.x + rect.x ) / scale ) + 'px'; + document.body.style.top = ( - ( scrollOffset.y + rect.y ) / scale ) + 'px'; + document.body.style.width = ( scale * 100 ) + '%'; + document.body.style.height = ( scale * 100 ) + '%'; + document.body.style.zoom = scale; + } + } + + level = scale; + + if( document.documentElement.classList ) { + if( level !== 1 ) { + document.documentElement.classList.add( 'zoomed' ); + } + else { + document.documentElement.classList.remove( 'zoomed' ); + } + } + } + + /** + * Pan the document when the mosue cursor approaches the edges + * of the window. + */ + function pan() { + var range = 0.12, + rangeX = window.innerWidth * range, + rangeY = window.innerHeight * range, + scrollOffset = getScrollOffset(); + + // Up + if( mouseY < rangeY ) { + window.scroll( scrollOffset.x, scrollOffset.y - ( 1 - ( mouseY / rangeY ) ) * ( 14 / level ) ); + } + // Down + else if( mouseY > window.innerHeight - rangeY ) { + window.scroll( scrollOffset.x, scrollOffset.y + ( 1 - ( window.innerHeight - mouseY ) / rangeY ) * ( 14 / level ) ); + } + + // Left + if( mouseX < rangeX ) { + window.scroll( scrollOffset.x - ( 1 - ( mouseX / rangeX ) ) * ( 14 / level ), scrollOffset.y ); + } + // Right + else if( mouseX > window.innerWidth - rangeX ) { + window.scroll( scrollOffset.x + ( 1 - ( window.innerWidth - mouseX ) / rangeX ) * ( 14 / level ), scrollOffset.y ); + } + } + + function getScrollOffset() { + return { + x: window.scrollX !== undefined ? window.scrollX : window.pageXOffset, + y: window.scrollY !== undefined ? window.scrollY : window.pageYOffset + } + } + + return { + /** + * Zooms in on either a rectangle or HTML element. + * + * @param {Object} options + * - element: HTML element to zoom in on + * OR + * - x/y: coordinates in non-transformed space to zoom in on + * - width/height: the portion of the screen to zoom in on + * - scale: can be used instead of width/height to explicitly set scale + */ + to: function( options ) { + + // Due to an implementation limitation we can't zoom in + // to another element without zooming out first + if( level !== 1 ) { + zoom.out(); + } + else { + options.x = options.x || 0; + options.y = options.y || 0; + + // If an element is set, that takes precedence + if( !!options.element ) { + // Space around the zoomed in element to leave on screen + var padding = 20; + var bounds = options.element.getBoundingClientRect(); + + options.x = bounds.left - padding; + options.y = bounds.top - padding; + options.width = bounds.width + ( padding * 2 ); + options.height = bounds.height + ( padding * 2 ); + } + + // If width/height values are set, calculate scale from those values + if( options.width !== undefined && options.height !== undefined ) { + options.scale = Math.max( Math.min( window.innerWidth / options.width, window.innerHeight / options.height ), 1 ); + } + + if( options.scale > 1 ) { + options.x *= options.scale; + options.y *= options.scale; + + magnify( options, options.scale ); + + if( options.pan !== false ) { + + // Wait with engaging panning as it may conflict with the + // zoom transition + panEngageTimeout = setTimeout( function() { + panUpdateInterval = setInterval( pan, 1000 / 60 ); + }, 800 ); + + } + } + } + }, + + /** + * Resets the document zoom state to its default. + */ + out: function() { + clearTimeout( panEngageTimeout ); + clearInterval( panUpdateInterval ); + + magnify( { x: 0, y: 0 }, 1 ); + + level = 1; + }, + + // Alias + magnify: function( options ) { this.to( options ) }, + reset: function() { this.out() }, + + zoomLevel: function() { + return level; + } + } + +})(); diff --git a/mstreams/presentation/plugin/zoom/zoom.esm.js b/mstreams/presentation/plugin/zoom/zoom.esm.js new file mode 100644 index 0000000..c0e8d7b --- /dev/null +++ b/mstreams/presentation/plugin/zoom/zoom.esm.js @@ -0,0 +1,4 @@ +/*! + * reveal.js Zoom plugin + */ +var e={id:"zoom",init:function(e){e.getRevealElement().addEventListener("mousedown",(function(n){var o=/Linux/.test(window.navigator.platform)?"ctrl":"alt",i=(e.getConfig().zoomKey?e.getConfig().zoomKey:o)+"Key",d=e.getConfig().zoomLevel?e.getConfig().zoomLevel:2;n[i]&&!e.isOverview()&&(n.preventDefault(),t.to({x:n.clientX,y:n.clientY,scale:d,pan:!1}))}))},destroy:function(){t.reset()}},t=function(){var e=1,n=0,o=0,i=-1,d=-1,l="transform"in document.body.style;function s(t,n){var o=r();if(t.width=t.width||1,t.height=t.height||1,t.x-=(window.innerWidth-t.width*n)/2,t.y-=(window.innerHeight-t.height*n)/2,l)if(1===n)document.body.style.transform="";else{var i=o.x+"px "+o.y+"px",d="translate("+-t.x+"px,"+-t.y+"px) scale("+n+")";document.body.style.transformOrigin=i,document.body.style.transform=d}else 1===n?(document.body.style.position="",document.body.style.left="",document.body.style.top="",document.body.style.width="",document.body.style.height="",document.body.style.zoom=""):(document.body.style.position="relative",document.body.style.left=-(o.x+t.x)/n+"px",document.body.style.top=-(o.y+t.y)/n+"px",document.body.style.width=100*n+"%",document.body.style.height=100*n+"%",document.body.style.zoom=n);e=n,document.documentElement.classList&&(1!==e?document.documentElement.classList.add("zoomed"):document.documentElement.classList.remove("zoomed"))}function c(){var t=.12*window.innerWidth,i=.12*window.innerHeight,d=r();owindow.innerHeight-i&&window.scroll(d.x,d.y+(1-(window.innerHeight-o)/i)*(14/e)),nwindow.innerWidth-t&&window.scroll(d.x+(1-(window.innerWidth-n)/t)*(14/e),d.y)}function r(){return{x:void 0!==window.scrollX?window.scrollX:window.pageXOffset,y:void 0!==window.scrollY?window.scrollY:window.pageYOffset}}return l&&(document.body.style.transition="transform 0.8s ease"),document.addEventListener("keyup",(function(n){1!==e&&27===n.keyCode&&t.out()})),document.addEventListener("mousemove",(function(t){1!==e&&(n=t.clientX,o=t.clientY)})),{to:function(n){if(1!==e)t.out();else{if(n.x=n.x||0,n.y=n.y||0,n.element){var o=n.element.getBoundingClientRect();n.x=o.left-20,n.y=o.top-20,n.width=o.width+40,n.height=o.height+40}void 0!==n.width&&void 0!==n.height&&(n.scale=Math.max(Math.min(window.innerWidth/n.width,window.innerHeight/n.height),1)),n.scale>1&&(n.x*=n.scale,n.y*=n.scale,s(n,n.scale),!1!==n.pan&&(i=setTimeout((function(){d=setInterval(c,1e3/60)}),800)))}},out:function(){clearTimeout(i),clearInterval(d),s({x:0,y:0},1),e=1},magnify:function(e){this.to(e)},reset:function(){this.out()},zoomLevel:function(){return e}}}();export default function(){return e} diff --git a/mstreams/presentation/plugin/zoom/zoom.js b/mstreams/presentation/plugin/zoom/zoom.js new file mode 100644 index 0000000..b52804d --- /dev/null +++ b/mstreams/presentation/plugin/zoom/zoom.js @@ -0,0 +1,4 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).RevealZoom=t()}(this,(function(){"use strict"; +/*! + * reveal.js Zoom plugin + */var e={id:"zoom",init:function(e){e.getRevealElement().addEventListener("mousedown",(function(o){var n=/Linux/.test(window.navigator.platform)?"ctrl":"alt",i=(e.getConfig().zoomKey?e.getConfig().zoomKey:n)+"Key",d=e.getConfig().zoomLevel?e.getConfig().zoomLevel:2;o[i]&&!e.isOverview()&&(o.preventDefault(),t.to({x:o.clientX,y:o.clientY,scale:d,pan:!1}))}))},destroy:function(){t.reset()}},t=function(){var e=1,o=0,n=0,i=-1,d=-1,l="transform"in document.body.style;function s(t,o){var n=r();if(t.width=t.width||1,t.height=t.height||1,t.x-=(window.innerWidth-t.width*o)/2,t.y-=(window.innerHeight-t.height*o)/2,l)if(1===o)document.body.style.transform="";else{var i=n.x+"px "+n.y+"px",d="translate("+-t.x+"px,"+-t.y+"px) scale("+o+")";document.body.style.transformOrigin=i,document.body.style.transform=d}else 1===o?(document.body.style.position="",document.body.style.left="",document.body.style.top="",document.body.style.width="",document.body.style.height="",document.body.style.zoom=""):(document.body.style.position="relative",document.body.style.left=-(n.x+t.x)/o+"px",document.body.style.top=-(n.y+t.y)/o+"px",document.body.style.width=100*o+"%",document.body.style.height=100*o+"%",document.body.style.zoom=o);e=o,document.documentElement.classList&&(1!==e?document.documentElement.classList.add("zoomed"):document.documentElement.classList.remove("zoomed"))}function c(){var t=.12*window.innerWidth,i=.12*window.innerHeight,d=r();nwindow.innerHeight-i&&window.scroll(d.x,d.y+(1-(window.innerHeight-n)/i)*(14/e)),owindow.innerWidth-t&&window.scroll(d.x+(1-(window.innerWidth-o)/t)*(14/e),d.y)}function r(){return{x:void 0!==window.scrollX?window.scrollX:window.pageXOffset,y:void 0!==window.scrollY?window.scrollY:window.pageYOffset}}return l&&(document.body.style.transition="transform 0.8s ease"),document.addEventListener("keyup",(function(o){1!==e&&27===o.keyCode&&t.out()})),document.addEventListener("mousemove",(function(t){1!==e&&(o=t.clientX,n=t.clientY)})),{to:function(o){if(1!==e)t.out();else{if(o.x=o.x||0,o.y=o.y||0,o.element){var n=o.element.getBoundingClientRect();o.x=n.left-20,o.y=n.top-20,o.width=n.width+40,o.height=n.height+40}void 0!==o.width&&void 0!==o.height&&(o.scale=Math.max(Math.min(window.innerWidth/o.width,window.innerHeight/o.height),1)),o.scale>1&&(o.x*=o.scale,o.y*=o.scale,s(o,o.scale),!1!==o.pan&&(i=setTimeout((function(){d=setInterval(c,1e3/60)}),800)))}},out:function(){clearTimeout(i),clearInterval(d),s({x:0,y:0},1),e=1},magnify:function(e){this.to(e)},reset:function(){this.out()},zoomLevel:function(){return e}}}();return function(){return e}})); diff --git a/post/index.html b/post/index.html new file mode 100644 index 0000000..97b1afe --- /dev/null +++ b/post/index.html @@ -0,0 +1,271 @@ + + + + + + + + Posts + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    Posts

    + + + + +
    + + + + + + + + +

    RSocket-JVM: streamlining implementation for each vendor platform

    +
    April 22, 2021
    + +RSocket + +Mstreams + +java + + + +
    + + + +
    + + + +
    + + + + + + + +
    + + + +
    + +
    + + + + + + + + diff --git a/post/index.xml b/post/index.xml new file mode 100644 index 0000000..ba2dc7e --- /dev/null +++ b/post/index.xml @@ -0,0 +1,126 @@ + + + + Posts on JAUNT.SDN + https://jauntsdn.com/post/ + Recent content in Posts on JAUNT.SDN + Hugo -- gohugo.io + en-us + Wed, 01 Feb 2023 10:18:00 +0200 + + + + + + 📌 Summary: alternative RSocket library for high performance network applications on JVM + https://jauntsdn.com/post/rsocket-summary/ + Wed, 01 Feb 2023 10:18:00 +0200 + + https://jauntsdn.com/post/rsocket-summary/ + Summary lean &amp; very fast GRPC-like services on JVM with rich streaming models over multiple data-center and internet transports. multiple APIs: CompletableFuture &amp; virtual threads; traditional streaming GRPC-stub (StreamObserver), or reactive with smallrye-mutiny, rxjava, reactor pluggable networking: tcp, unix sockets, VM sockets; grpc, websockets, websockets-over-http2 service APIs / codegen stubs (Message-Streams) are split from library runtime (RSocket-JVM, including network transports, load estimators, load balancers, metrics) transparent origin (RPC) &amp; proxy load estimation for low-cost yet efficient load balancers native image support with graalvm Project consists of API modules for each vendor library - Message-Streams, and respective runtime implementations - RSocket-JVM. + + + + Jaunt-RSocket-RPC, Spring-RSocket, GRPC: quantitative and qualitative comparison + https://jauntsdn.com/post/rsocket-vs-spring/ + Fri, 03 Sep 2021 10:18:00 +0200 + + https://jauntsdn.com/post/rsocket-vs-spring/ + Earlier this year we described weaknesses of Spring RSocket-java implementation that resulted in awful throughput, latency &amp; garbage production: key metrics for core networking library. +Latest Spring-RSocket uses RSocket/RSocket-java version 1.1.1, and wraps up almost a year of progress made on the library. +The goal of this post is to re-evaluate that progress against both current jauntsdn/RSocket-RPC (not yet publicly available), and grpc/grpc-java. +Jauntsdn-RSocket-RPC vs Spring-RSocket-CBOR: quantitative comparison jauntsdn/RSocket-RPC is remote procedure call system using streaming libraries (GRPC-API or several reactive - including RSocket-reactor) for networking &amp; APIs, and Protocol Buffers as sole data format. + + + + Alternative RSocket-RPC: fast application services communication and transparent GRPC bridge + https://jauntsdn.com/post/rsocket-grpc/ + Thu, 20 May 2021 08:00:00 +0200 + + https://jauntsdn.com/post/rsocket-grpc/ + Exchanging millions of buffers per second with plain RSockets is not useful for business applications because they need structured data instead of raw bytes. +That&rsquo;s why there was RSocket-RPC - remote procedure call system on top of Protocol Buffers with language agnostic service definitions. It uses code-generation for performance equivalent to hand-written code. +Protocol buffers have tiny on-wire overhead, acceptable performance and is native format of Grpc - important for interoperability and available tooling that can be used directly, or easily adapted. + + + + RSocket-JVM: streamlining implementation for each vendor platform + https://jauntsdn.com/post/rsocket-jvm/ + Thu, 22 Apr 2021 08:00:00 +0200 + + https://jauntsdn.com/post/rsocket-jvm/ + Motivation In practice reactive applications - as a method to model software and programming paradigm of composable flow controlled streams - viable mainly within JVM context. +It has necessary ecosystem provided by several competing platforms: smallrye-mutiny by Red Hat, project reactor by (ex-)Pivotal, and community-maintained rxjava. +Libraries are interoperable on same VM because their APIs semantically conform to reactive-streams specification, with 3 of 4 having binary compatibility - streams may be combined directly without writing any adapter code. + + + + Serving one million streams. Part 2. Double performance over original RSocket library + https://jauntsdn.com/post/rsocket-million-streams-2/ + Wed, 20 Jan 2021 10:00:00 +0200 + + https://jauntsdn.com/post/rsocket-million-streams-2/ + New year break is good time for continuing on topic of one million of streams - serving huge amount of RSocket streams simultaneously with single, mid-level commodity computer. +The original library gave in around 500k streams - mark where server was still stable, at whopping 500-600 MBytes/s allocation rate - unexpected numbers for RSockets just sending same byte buffer periodically, using library advertising zero-copy capability. +Interesting question is whether we hit natural limit, caused by current state of JVM runtime and libraries, or is a consequence of design choices driven at large by &ldquo;ideological&rdquo; and marketing motives that eventually resulted in implementation having significant parts happen to exist just for burning CPU cycles - both directly and through garbage collection? + + + + WebSockets over http/2: implementing RFC8441 with Netty + https://jauntsdn.com/post/netty-websocket-http2/ + Thu, 30 Jul 2020 11:00:00 +0200 + + https://jauntsdn.com/post/netty-websocket-http2/ + Today I&rsquo;d like to introduce you to netty-websocket-http2 - implementation of websockets-over-http2, first release of which is about to land on the Maven Central. +Novel protocol graduated out of draft phase in September 2018, and is one of the smallest RFCs I&rsquo;ve ever seen - just 4 pages of actual specification. +Despite tiny size It opens some interesting possibilities - for both clients and servers. +Protocol defines Extended CONNECT Method - mechanism for establishing byte stream tunnel over single http2 stream, and addresses biggest flaw of websocket over http1 - need for a separate tcp connection per websocket. + + + + Serving one million streams. Part 1. RSocket limits on the JVM + https://jauntsdn.com/post/rsocket-million-streams-1/ + Wed, 24 Jun 2020 19:30:00 +0200 + + https://jauntsdn.com/post/rsocket-million-streams-1/ + One of the interesting topics is estimation of RSocket limits in setup that approximates gateway serving tens of thousands clients. +There are numerous reports focusing on few streams over single connection, with emphasis on huge throughput. However this mode is not intrinsical for real server. Even though gateways are more likely to be cpu bound than IO bound, we can relax this assumption by making server logic trivial (just echo back incoming messages), to see how RSocket behaves under idealized conditions when major resource consumption is related to IO. + + + + Service concurrency limiting with RSocket request leases + https://jauntsdn.com/post/rsocket-lease-concurrency-limiting/ + Thu, 09 Apr 2020 17:00:00 +0200 + + https://jauntsdn.com/post/rsocket-lease-concurrency-limiting/ + This is first post in advanced applications of RSocket series, and topic for today is less known yet essential protocol feature - requests leasing. +We know that single RSocket stream is naturally bounded due to Reactive Streams semantics - there is no more in-flight messages than demanded with receiver Subscription. +However limiting outstanding messages per request is not useful without limiting requests concurrency. +For services, growing requests unbounded exhausts their backing resources and leads to latency surge. + + + + RSocket: short introduction + https://jauntsdn.com/post/rsocket-intro/ + Tue, 17 Mar 2020 11:30:00 +0200 + + https://jauntsdn.com/post/rsocket-intro/ + Description from official RSocket-java repository +RSocket is a binary protocol for use on byte stream transports... It enables the following symmetric interaction models via async message passing over a single connection.. leaves bits of mystery unless reader is interested enough to skim over lengthy FAQ and motivations documents. This post is one page size summary of distinctive RSocket features that make It a good companion for Http2/GRPC. +Low latency RSocket provides means for maintaining low latency under high throughput by limiting incoming and outgoing message queue size on both sides of connection: + + + + IO_URING for Netty based RPC: quick analysis + https://jauntsdn.com/post/rpc-iouring/ + Sat, 02 Mar 2019 08:00:00 +0200 + + https://jauntsdn.com/post/rpc-iouring/ + Netty IO_URING based transport was recently released as incubator project. Several sources have reported substantial performance gains with It, including Netty team TCP echo test results that demonstrated 2x better throughput compared to EPOLL. +The goal of this post is CPU and memory performance evaluation of IO_URING in RPC library implemented with Netty on top of TCP, and comparison against EPOLL IO. +It focuses on two use cases: + backend service: few dozens of connections, each serving ~1000 concurrent streams of few dozens messages each + + + + \ No newline at end of file diff --git a/post/netty-websocket-http1/index.html b/post/netty-websocket-http1/index.html new file mode 100644 index 0000000..f9c311c --- /dev/null +++ b/post/netty-websocket-http1/index.html @@ -0,0 +1,355 @@ + + + + + + + + Alternative WebSockets for netty/java: doubling throughput of small messages + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +

    Alternative WebSockets for netty/java: doubling throughput of small messages

    +
    January 9, 2023
    + +netty + +websocket + +java + + +
    + +

    TL;DR

    +

    This post is brief presentation of netty-websocket-http1: +alternative netty/java implementation of RFC6455 - the WebSocket protocol.

    +

    Its advantage is significant per-core throughput improvement (1.8–2x) for small frames in comparison to netty’s +out-of-the-box websocket codecs, and minimal heap allocations on frame path. Library may also be combined with +netty-websocket-http2.

    +

    Its purpose is to be the basis for high-performance RPC transport of small binary messages (protocol buffers), +mainly cross-datacenter communications over internet — both http1 and http2.

    +

    Preliminary performance evaluation with netty’s out-of-the-box codec showed only ~1M 120 byte messages per core +over non-TLS connection — very modest for this simple wire format, concluding from experience with other +protocols.

    +

    Additionally there are unnecessary per-frame allocations as netty’s codec expects binary payloads wrapped as +WebSocketFrame messages (which are likely not useful for users application purposes), plus allocates array per-frame +for payload masking (latter was recently improved).

    +

    use case & scope

    +
      +
    • +

      Intended for efficiently encoded, dense binary data: no extensions (compression) support / outbound text frames / +inbound UTF8 validation.

      +
    • +
    • +

      Library assumes small frames — many have payload <= 125 bytes, most are < 1500, maximum supported is 65k (65535 bytes).

      +
    • +
    • +

      Just codec — fragments, pings, close frames are decoded & validated only. It is responsibility of user code to +handle frames according to protocol (reassemble frame fragments, perform graceful close, respond to pings).

      +
    • +
    • +

      Dedicated decoder for case of exchanging tiny messages over TLS connection: only non-masked frames with <= 125 bytes +of payload for minimal per-WebSocket state (memory) overhead.

      +
    • +
    • +

      No per-frame heap allocations in WebSocket FrameFactory / decoder.

      +
    • +
    • +

      Single-threaded (transport IO event-loop) callbacks / frame factory API: in practice user code has its own message +types to carry data, external means (e.g. mpsc / spsc queues) may be used to properly publish messages on event-loop thread.

      +
    • +
    +

    FrameFactory / Callbacks API

    +

    1.WebSocketFrameFactory to create outbound frames as plain byte buffers, which helps to reduce pressure on memory +allocator, and avoid either two tiny buffers (“header” plus payload) or redundant memory copies for each frame.

    +

    It is library user responsibility to mask outbound frame once payload is written: ByteBuf WebSocketFrameFactory.mask(ByteBuf)

    +
    public interface WebSocketFrameFactory {
    +
    +  ByteBuf createBinaryFrame(ByteBufAllocator allocator, int binaryDataSize);
    +  
    +  // create*Frame are omitted for control frames, created in similar fashion
    +
    +  ByteBuf mask(ByteBuf frame);
    +}
    +

    2.WebSocketFrameListener to receive inbound frames

    +
    public interface WebSocketFrameListener {
    +
    +  void onChannelRead(ChannelHandlerContext ctx, boolean finalFragment,
    +                     int rsv, int opcode, ByteBuf payload);
    +   
    +  // netty handler callbacks are omitted for brevity
    +
    +  // lifecycle
    +  default void onOpen(ChannelHandlerContext ctx) {}
    +
    +  default void onClose(ChannelHandlerContext ctx) {}
    +

    3.WebSocketCallbacksHandler to exchange WebSocketFrameListener for WebSocketFrameFactory on successful +WebSocket handshake

    +
    public interface WebSocketCallbacksHandler {
    +
    +  WebSocketFrameListener exchange(
    +      ChannelHandlerContext ctx, WebSocketFrameFactory webSocketFrameFactory);
    +}
    +

    4.Similar to Netty, this library has WebSocketClientProtocolHandler & WebSocketServerProtocolHandler for end users.

    +

    These handlers are responsible for whole WebSocket http handshake process — up until WebSocketCallbacksHandler +exchange on successful handshake completion.

    +

    It is common for WebSocketCallbacksHandler to also implement WebSocketFrameListener, so users have

    +
    class FrameHandler implements WebSocketCallbacksHandler, 
    +                              WebSocketFrameListener {
    +
    +  WebSocketFrameFactory webSocketFrameFactory;
    +
    +  WebSocketFrameListener exchange(
    +        ChannelHandlerContext ctx, 
    +        WebSocketFrameFactory webSocketFrameFactory) {
    +    this.webSocketFrameFactory = webSocketFrameFactory;
    +  }
    +
    +  void onChannelRead(ChannelHandlerContext ctx, 
    +      boolean finalFragment, int rsv, int opcode, ByteBuf payload) {
    +    // read inbound frames, write outbound frames /w webSocketFrameFactory
    +  }
    +}
    +

    Performance test module serves as good API showcase for both client +and server.

    +

    performance

    +

    Below is per-core throughput +comparison with netty’s out-of-the-box WebSocket handlers: non-masked frames with 8, 64, 125, 1000 bytes of randomized +payload over encrypted/non-encrypted connection.

    +
      +
    • non-encrypted
    • +
    +
    + +
    + +
      +
    • encrypted
    • +
    +
    + +
    + +

    websocket-over-http2

    +

    One drawback of websocket-over-http2 support with OOTB netty codecs is that It is much slower than either +http2 or WebSocket alone (2 protocols to decode from byte stream).

    +

    This library helps to ease the problem as It may be combined with jauntsdn/websocket-http2 +using http1 codec +API for comparable benefit. With 8, 125, 1000 bytes of randomized payload frames over encrypted connection results are as follows:

    +
    + +
    + +
    + + + + + + + + + + + + + + +
    + +
    + +
    + + + + + + + + diff --git a/post/netty-websocket-http2/index.html b/post/netty-websocket-http2/index.html new file mode 100644 index 0000000..c0596d8 --- /dev/null +++ b/post/netty-websocket-http2/index.html @@ -0,0 +1,432 @@ + + + + + + + + WebSockets over http/2: implementing RFC8441 with Netty + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +

    WebSockets over http/2: implementing RFC8441 with Netty

    +
    July 30, 2020
    + +netty + +websocket + +http2 + +java + + +
    + +

    Today I’d like to introduce you to netty-websocket-http2 - implementation of +websockets-over-http2, first release of which is about to land on the Maven Central.

    +

    Novel protocol graduated out of draft phase in September 2018, and is one of the smallest RFCs I’ve ever seen - +just 4 pages of actual specification.

    +

    Despite tiny size It opens some interesting possibilities - for both clients and servers.

    +

    Protocol defines Extended CONNECT Method - mechanism for establishing byte +stream tunnel over single http2 stream, and addresses biggest flaw of websocket over http1 - need for a separate tcp +connection per websocket.

    +

    This benefits servers because clients use single connection for both http and rpc calls; lowers client latency since tcp & tls handshake steps +are eliminated; enables simpler gateways - same software stack for internet traffic implemented in terms of http2 streams; +adds priority support for http/rpc requests of the same connection.

    +

    One drawback is limited widespread - the only first-tier browser having protocol support is Mozilla Firefox. +However libwebsockets - popular C networking library - also supports RFC8441, +which means It becomes viable option if primary clients are native mobile.

    +

    Update from May 2021.
    +Chrome web browser supports websockets-over-http2, which together with Firefox constitutes +~85% of web clients. On server side protocol is supported by haproxy +and envoy proxy.

    +

    Scope

    +

    Library is addressing 2 use cases: for application servers and clients, +It is transparent use of existing http1 websocket handlers on top of http2 streams; for gateways/proxies, +It is websockets-over-http2 support with no http1 dependencies and minimal overhead.

    +

    websocket channel API

    +

    Intended for application servers and clients.
    +Allows transparent usage of existing http1 websocket handlers on top of http2 stream.

    +
      +
    • Server
    • +
    +
    EchoWebSocketHandler http1WebSocketHandler = new EchoWebSocketHandler();
    +
    + Http2WebSocketServerHandler http2webSocketHandler =
    +       Http2WebSocketServerBuilder.create()
    +              .acceptor(
    +                   (ctx, path, subprotocols, request, response) -> {
    +                     switch (path) {
    +                       case "/echo":
    +                         if (subprotocols.contains("echo.jauntsdn.com")
    +                             && acceptUserAgent(request, response)) {
    +                           /*selecting subprotocol for accepted requests is mandatory*/
    +                           Http2WebSocketAcceptor.Subprotocol
    +                                  .accept("echo.jauntsdn.com", response);
    +                           return ctx.executor()
    +                                  .newSucceededFuture(http1WebSocketHandler);
    +                         }
    +                         break;
    +                       case "/echo_all":
    +                         if (subprotocols.isEmpty() 
    +                                  && acceptUserAgent(request, response)) {
    +                           return ctx.executor()
    +                                  .newSucceededFuture(http1WebSocketHandler);
    +                         }
    +                         break;
    +                     }
    +                     return ctx.executor()
    +                         .newFailedFuture(
    +                             new WebSocketHandshakeException(
    +                                  "websocket rejected, path: " + path));
    +                   })
    +              .build();
    +
    +      ch.pipeline()
    +           .addLast(sslHandler, 
    +                    http2frameCodec, 
    +                    http2webSocketHandler);
    +
      +
    • Client
    • +
    +
     Channel channel =
    +        new Bootstrap()
    +            .handler(
    +                new ChannelInitializer<SocketChannel>() {
    +                  @Override
    +                  protected void initChannel(SocketChannel ch) {
    +
    +                    Http2WebSocketClientHandler http2WebSocketClientHandler =
    +                        Http2WebSocketClientBuilder.create()
    +                            .handshakeTimeoutMillis(15_000)
    +                            .build();
    +
    +                    ch.pipeline()
    +                        .addLast(
    +                            sslHandler,
    +                            http2FrameCodec,
    +                            http2WebSocketClientHandler);
    +                  }
    +                })
    +            .connect(address)
    +            .sync()
    +            .channel();
    +
    +Http2WebSocketClientHandshaker handShaker = 
    +            Http2WebSocketClientHandshaker.create(channel);
    +
    +Http2Headers headers =
    +   new DefaultHttp2Headers()
    +            .set("user-agent", "jauntsdn-websocket-http2-client/1.1.2");
    +
    +ChannelFuture handshake =
    +   /*http1 websocket handler*/
    +   handShaker.handshake("/echo", headers, new EchoWebSocketHandler());
    +    
    +handshake.channel().writeAndFlush(new TextWebSocketFrame("hello http2 websocket"));
    +

    Successfully handshaked http2 stream spawns websocket subchannel, and provided http1 websocket handlers are added +to its pipeline.

    +

    Runnable demo is available in netty-websocket-http2-example module - +channelserver, +channelclient.

    +

    websocket handshake only API

    +

    Intended for intermediaries/proxies.
    +Only verifies whether http2 stream is valid websocket, then passes it down the pipeline as POST request with x-protocol=websocket header.

    +
          Http2WebSocketServerHandler http2webSocketHandler =
    +          Http2WebSocketServerBuilder.buildHandshakeOnly();
    +
    +      Http2StreamsHandler http2StreamsHandler = new Http2StreamsHandler();
    +      ch.pipeline()
    +           .addLast(sslHandler, 
    +                    frameCodec, 
    +                    http2webSocketHandler, 
    +                    http2StreamsHandler);
    +

    Works with both callbacks-style Http2ConnectionHandler and frames based Http2FrameCodec.

    +

    Runnable demo is available in netty-websocket-http2-example module - +handshakeserver, +channelclient.

    +

    compression & subprotocols

    +

    Server/client permessage-deflate compression configuration is shared by all streams

    +
    Http2WebSocketServerBuilder.compression(enabled);
    +

    or

    +
    Http2WebSocketServerBuilder.compression(
    +      compressionLevel,
    +      allowServerWindowSize,
    +      preferredClientWindowSize,
    +      allowServerNoContext,
    +      preferredClientNoContext);
    +

    Client subprotocols are configured on per-path basis

    +
    ChannelFuture handshake =
    +        handShaker.handshake("/echo", "subprotocol", headers, new EchoWebSocketHandler());
    +

    On a server It is responsibility of Http2WebSocketAcceptor to select supported protocol with

    +
    Http2WebSocketAcceptor.Subprotocol.accept(subprotocol, response);
    +

    lifecycle

    +

    Handshake events and several shutdown options are available when +using Websocket channel style APIs.

    +

    handshake events

    +

    Events are fired on parent channel, also on websocket channel if one gets created

    +
      +
    • Http2WebSocketHandshakeStartEvent (correlationId, path, subprotocols, timestampNanos, requestHeaders)
    • +
    • Http2WebSocketHandshakeErrorEvent (correlationId, path, subprotocols, timestampNanos, responseHeaders, error)
    • +
    • Http2WebSocketHandshakeSuccessEvent (correlationId, path, subprotocols, timestampNanos, responseHeaders)
    • +
    +

    graceful shutdown

    +

    Outbound Http2WebSocketLocalCloseEvent on websocket channel pipeline shuts down +http2 stream by sending empty DATA frame with END_STREAM flag set.

    +

    Graceful stream shutdown by remote is represented with inbound Http2WebSocketRemoteCloseEvent on +websocket channel pipeline, graceful connection shutdown - with Http2WebSocketRemoteGoAwayEvent.

    +

    shutdown

    +

    Closing websocket channel terminates its http2 stream by sending RST frame.

    +

    validation & write error events

    +

    Both API style handlers send Http2WebSocketHandshakeErrorEvent for invalid websocket-over-http2 and http requests. +For http2 frame write errors Http2WebSocketWriteErrorEvent is sent on parent channel if auto-close is not enabled; +otherwise exception is delivered with ChannelPipeline.fireExceptionCaught followed by immediate close.

    +

    flow control

    +

    Inbound flow control is done automatically as soon as DATA frames are received. +Library relies on netty’s DefaultHttp2LocalFlowController for refilling receive window.

    +

    Outbound flow control is expressed as websocket channels writability change on send window +exhaust/refill, provided by DefaultHttp2RemoteFlowController.

    +

    websocket stream weight

    +

    Initial stream weight is configured with

    +
    Http2WebSocketClientBuilder.streamWeight(weight);
    +

    it can be updated by firing Http2WebSocketStreamWeightUpdateEvent on websocket channel pipeline. +Currently blocked by netty bug.

    +

    performance

    +

    Library relies on capabilities provided by netty’s Http2ConnectionHandler so performance characteristics should be similar. +netty-websocket-http2-perftest +module contains application that gives rough throughput/latency estimate. The application is started with perf_server.sh, perf_client.sh.

    +

    On modern box one can expect following results for single websocket:

    +
    19:31:58.537 epollEventLoopGroup-2-1 com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.client.Main p50 => 435 micros
    +19:31:58.537 epollEventLoopGroup-2-1 com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.client.Main p95 => 662 micros
    +19:31:58.537 epollEventLoopGroup-2-1 com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.client.Main p99 => 841 micros
    +19:31:58.537 epollEventLoopGroup-2-1 com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.client.Main throughput => 205874 messages
    +19:31:58.537 epollEventLoopGroup-2-1 com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.client.Main throughput => 201048.83 kbytes
    +
    +

    To evaluate performance with multiple connections we compose an application comprised with simple echo server, and client +sending batches of messages periodically over single websocket per connection (approximately models chat application).

    +

    With 25k active connections each sending batches of 5-10 messages of 0.2-0.5 KBytes over single websocket every 15-30seconds, +the results are as follows (measured over time spans of 5 seconds):

    +
    11:32:44.080 pool-2-thread-1 com.jauntsdn.netty.handler.codec.http2.websocketx.stresstest.client.Main connection success   ==> 25000
    +11:32:44.080 pool-2-thread-1 com.jauntsdn.netty.handler.codec.http2.websocketx.stresstest.client.Main handshake success    ==> 25000
    +11:32:44.080 pool-2-thread-1 com.jauntsdn.netty.handler.codec.http2.websocketx.stresstest.client.Main messages p99, micros ==> 177
    +11:32:44.080 pool-2-thread-1 com.jauntsdn.netty.handler.codec.http2.websocketx.stresstest.client.Main messages p50, micros ==> 91
    +

    examples

    +

    netty-websocket-http2-example +module contains demos showcasing both API styles, +with this library/browser as clients.

    +
      +
    • channelserver, channelclient packages for websocket subchannel API demos.
    • +
    • handshakeserver, channelclient packages for handshake only API demo.
    • +
    • lwsclient package for client demo that runs against https://libwebsockets.org/testserver/ which hosts websocket-over-http2 +server implemented with libwebsockets - popular C-based networking library.
    • +
    +

    browser example

    +

    Channelserver example serves web page at https://www.localhost:8099 that sends pings to /echo endpoint.

    +

    Currently only Mozilla Firefox and latest Google Chrome support websockets-over-http2.

    +
    + + + + + + + + + + + + + + +
    + +
    + +
    + + + + + + + + diff --git a/post/page/1/index.html b/post/page/1/index.html new file mode 100644 index 0000000..4ca6107 --- /dev/null +++ b/post/page/1/index.html @@ -0,0 +1 @@ +https://jauntsdn.com/post/ \ No newline at end of file diff --git a/post/page/2/index.html b/post/page/2/index.html new file mode 100644 index 0000000..feab7ff --- /dev/null +++ b/post/page/2/index.html @@ -0,0 +1,269 @@ + + + + + + + + Posts + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    Posts

    + +
    + + + + + + + + +

    WebSockets over http/2: implementing RFC8441 with Netty

    +
    July 30, 2020
    + +netty + +websocket + +http2 + +java + + + +
    +
    + + + + + + + + +

    Serving one million streams. Part 1. RSocket limits on the JVM

    +
    June 24, 2020
    + +RSocket + +Mstreams + +java + + + +
    +
    + + + + + + + + +

    Service concurrency limiting with RSocket request leases

    +
    April 9, 2020
    + +RSocket + +Mstreams + +java + +load-balancer + + + +
    +
    + + + + + + + + +

    RSocket: short introduction

    +
    March 17, 2020
    + +RSocket + +Mstreams + + + +
    +
    + + + + + + + + +

    IO_URING for Netty based RPC: quick analysis

    +
    March 2, 2019
    + +io_uring + +netty + + + +
    + + +
    + + + +
    + + + + + + + +
    + + + +
    + +
    + + + + + + + + diff --git a/post/page/3/index.html b/post/page/3/index.html new file mode 100644 index 0000000..9c89912 --- /dev/null +++ b/post/page/3/index.html @@ -0,0 +1,189 @@ + + + + + + + + Posts + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    Posts

    + +
    + + + + + + + + +

    IO_URING for Netty based RPC: quick analysis

    +
    March 2, 2019
    + +io_uring + +netty + + + +
    + + +
    + + + +
    + + + + + + + +
    + + + +
    + +
    + + + + + + + + diff --git a/post/rpc-iouring/index.html b/post/rpc-iouring/index.html new file mode 100644 index 0000000..32e67a8 --- /dev/null +++ b/post/rpc-iouring/index.html @@ -0,0 +1,372 @@ + + + + + + + + IO_URING for Netty based RPC: quick analysis + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +

    IO_URING for Netty based RPC: quick analysis

    +
    March 2, 2019
    + +io_uring + +netty + + +
    + +

    Netty IO_URING based transport was recently released +as incubator project. Several sources have reported substantial performance gains with It, including +Netty team TCP echo test results that demonstrated 2x better throughput compared to EPOLL.

    +

    The goal of this post is CPU and memory performance evaluation of IO_URING in RPC library implemented with Netty +on top of TCP, and comparison against EPOLL IO.

    +

    It focuses on two use cases:

    +
      +
    • +

      backend service: few dozens of connections, each serving ~1000 concurrent streams of few dozens messages each

      +
    • +
    • +

      frontend service: thousands of connections, each serving dozens of long-running streams, streams are periodical message +batches

      +
    • +
    +

    IO_URING

    +

    IO_URING is newest API for doing async I/O on Linux.

    +

    IO_URING has 2 ringbuffer backed queues shared between kernel and application process: one for submission of +syscalls that will be eventually executed by kernel, one for completion events.

    +

    io_uring_enter syscall is used to notify kernel about pending entries on submission queue.

    +

    IO_URING may reduce user-kernel space switch overhead, proportional to number of entries on submission queue +per io_uring_enter.

    +

    Netty: IO_URING based IO

    +

    Netty creates one IO_URING instance per event loop, its submission queue can have 4096 (allowed maximum) entries by default.

    +

    Transport relies on normal syscalls - pollin, pollout for network buffers readability/writability notifications, +followed by read and writev for reads/writes. Syscalls are scheduled as entries on submission queue, and results are consumed on +completion queue.

    +

    Transport maintains 1 outstanding read & writev submission per channel; channel writability state and available outbound +buffer capacity are updated asynchronously relative to Channel.flush() - only after corresponding writev result is received +on completion queue.

    +

    For this reason there may be only 1 outstanding flush() per Channel - consequent flushes are ignored until next write result.

    +

    Comparison with EPOLL

    +

    EPOLL is purely IO notification mechanism - Netty sets It in edge-triggered mode so clients are aware when inbound/outbound +buffers become readable/writeable for set of socket descriptors.

    +

    Contrary to IO_URING, Channel.flush() does synchronous writev syscall - +channel writability and available outbound buffer capacity are also updated synchronously.

    +

    Setup

    +

    Test setup is 1 client and 1 server sharing single host - commodity box having 12 vCPU/32GB/1GBit ethernet.

    +

    RPC library is part of not yet publicly available project com.jauntsdn.rsocket.

    +

    Kernel version: 5.10.1
    +Java version: 1.8.0_282 OpenJDK 64-Bit Server VM
    +Netty version: 4.1.59.Final
    +Transport IO_URING version: 0.0.4.Final

    +

    Backend service: streams throughput

    +

    1K of concurrent finite streams per connection, 32 messages per stream, message size is 128 bytes, 30 seconds ramp up.

    +

    Connection count varies in range 16-64.

    +
    Connection    IO transport         Host CPU usage,
    +count                              Server/Client
    +
    +16            EPOLL                0.05 / 0.05
    +              IO_URING             0.05 / 0.06
    +
    +32            EPOLL                0.10 / 0.13
    +              IO_URING             0.11 / 0.16
    +
    +48            EPOLL                0.15 / 0.24
    +              IO_URING             0.16 / 0.26
    +
    +64            EPOLL                0.20 / 0.37
    +              IO_URING             0.22 / 0.38
    +

    Frontend service: periodic message batches

    +

    Infinite stream of message batches: 20K connections, 30 streams per connection, 10 messages every 10 seconds, +100 seconds ramp up.

    +

    Message size varies in range 128 - 512 bytes.

    +
    Message       IO transport      Host CPU usage,
    +size, bytes                     Server/Client
    +
    +128           EPOLL             0.09 / 0.22
    +              IO_URING          0.10 / 0.22
    +
    +256           EPOLL             0.09 / 0.23
    +              IO_URING          0.11 / 0.23
    +
    +512           EPOLL             0.09 / 0.23
    +              IO_URING          0.11 / 0.24
    +

    Numbers follow backend service use case: slight IO_URING disadvantage for both reads and writes.

    +

    For both cases flame graphs showed there is no interesting discrepancies between EPOLL / IO_URING on client and server, +other than 7% of total CPU time spent in native code of IO_URING (I omit graphs for brevity as they are not very readable +in context of a blog).

    +

    This correlates with observed measurements. It seems the number of IO syscalls scheduled per io_uring_enter +is so small the cost of io_uring_enter itself overweights context switch and JNI overhead of individual reads/writes.

    +

    Next step would be measuring number of entries per submit & writev iovec size, but transport does not offer any metrics, +and I don’t have cycles to implement them atm.

    +

    Frontend service: periodic messages

    +

    Infinite stream of messages: 1 message every 1 second, 100 streams per connection, 8K connections, +200 seconds ramp up.

    +

    Message size varies in range 128 - 512 bytes.

    +
    Message       IO transport      Host CPU usage,
    +size, bytes                     Server/Client
    +
    +128           EPOLL             0.24 / 0.24
    +              IO_URING          0.18 / 0.12
    +
    +256           EPOLL             0.26 / 0.24
    +              IO_URING          0.18 / 0.13
    +
    +512           EPOLL             0.29 / 0.25
    +              IO_URING          0.20 / 0.16
    +

    Now IO_URING demonstrates better CPU performance for both reads and writes.

    +

    Client flamegraphs for periodic have one peculiarity in native callstack:

    +
    io_queue_async_work
    +-------------
    +io_queue_sqe
    +

    while for batch use case there is

    +
    io_read | io_write
    +-------------
    +io_issue_sqe
    +-------------
    +io_queue_sqe
    +

    Difference is caused by IOSQE_ASYNC flag. +It is set in transport if submission queue size exceeds given value on io_uring_enter, and is 25 +by default

    +

    Forcing IOSQE_ASYNC flag off did not have observable negative impact on CPU performance, but gave valuable hint - +periodic case performs better mainly due to more entries enqueued per submit compared to batch case.

    +

    To summarize, IO_URING transport gains vary on use case, may perform worse than EPOLL - profile your application to make informed choice.

    +

    Single stream throughput

    +

    Single continuous infinite stream over single connection, 1K message size, window size is 700K messages.

    +

    Window size is intentionally set to extreme value to highlight IO_URING transport specifics.

    +

    This case is not realistic and intended to demonstrate async nature of flushes with IO_URING.

    +

    EPOLL

    +

    Throughput, messages per second

    +
    11:08:31.656 netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 2994233
    +11:08:32.656 netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 3008608
    +11:08:33.656 netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 3006733
    +11:08:34.656 netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 3003608
    +11:08:35.656 netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 2902706
    +11:08:36.656 netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 3002358
    +11:08:37.656 netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 2986733
    +11:08:38.656 netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 2974233
    +11:08:39.656 netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 3002983
    +11:08:40.656 netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 2998608
    +

    IO_URING

    +

    Throughput, messages per second

    +
    11:36:30.598 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 1462272
    +11:36:31.598 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 871424
    +11:36:32.598 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 777216
    +11:36:33.598 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 2249728
    +11:36:34.598 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 1446912
    +11:36:35.598 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 1747968
    +11:36:36.598 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 1555456
    +11:36:37.598 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 1555456
    +11:36:38.598 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 1555456
    +11:36:39.598 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 1556478
    +11:36:40.598 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 2057216
    +11:36:41.598 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 1763328
    +11:36:42.598 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 1173504
    +

    Single stream throughput test is not in favor of IO_URING transport: 0.8 - 2.3 million messages per second versus 3 million with EPOLL.

    +

    To explain instability let’s look into allocations & GC.

    +

    Garbage production

    +

    EPOLL

    +

    Server GC over 1 minute with EPOLL
    +GC time: 1 collections, 8.468 ms Last Cause: G1 Evacuation Pause

    +

    Server peak heap memory usage is around 35 MBytes.

    +

    Small allocation rates are expected given that library under test creates nonexistent amount of per message garbage, +and request bytebuffer is reused for response stream.

    +

    And small heap size is consequence of sync flushes under EPOLL IO - there is limited number of inflight ByteBufs +and backed native memory so they are pooled efficiently.

    +

    IO_URING

    +

    Server GC over 1 minute with IO_URING
    +GC time: 67 collections, 24.933 s Last Cause: G1 Evacuation Pause

    +

    Server peak heap memory usage is 3.7 GBytes

    +

    Here are top allocations

    +
      bytes       percent  samples  top
    +  ----------  -------  -------  ---
    +  6177757184   64.87%     5898  io.netty.channel.ChannelOutboundBuffer$Entry
    +  2543085568   26.70%     2432  io.netty.util.Recycler$DefaultHandle
    +   793051136    8.33%      761  io.netty.buffer.PooledUnsafeDirectByteBuf
    +     6279168    0.07%        6  io.netty.buffer.PoolSubpage
    +     3141632    0.03%        3  io.netty.channel.unix.IovArray
    +

    Lot of ChannelOutboundBuffer entries for native buffers that await scheduling on submission queue is consequence +of async flushing with IO_URING.

    +

    Switching to more realistic 70K messages window still demonstrates IO_URING transport tendency for +elevated memory usage and GC, with lower throughput limit:

    +
    13:01:16.665 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 2021374
    +13:01:17.665 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 2206720
    +13:01:18.665 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 2254848
    +13:01:19.665 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 2333696
    +13:01:20.665 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 2410496
    +13:01:21.665 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 2227200
    +13:01:22.665 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 2284544
    +13:01:23.665 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 2331648
    +13:01:24.665 netty-io-transport-io_uring-1-1 com.jauntsdn.rsocket.test.client.Main client received messages: 2255872
    +

    GC time: 35 collections, 2.196 s Last Cause: G1 Evacuation Pause

    +

    Peak heap usage is 850 MBytes - order of magnitude more than EPOLL.

    +

    Single stream throughput is 2.2 - 2.4 million messages per second, versus 3 million with EPOLL. While such workload rarely +appears in practice, numbers are indicator of per message round trip overhead that manifests itself on production workloads, +but in smaller extent.

    +
    + + + + + +
    + + + + + + + + +

    WebSockets over http/2: implementing RFC8441 with Netty

    +
    July 30, 2020
    + +netty + +websocket + +http2 + +java + + + +
    + + + + + + +
    + +
    + +
    + + + + + + + + diff --git a/post/rsocket-grpc/index.html b/post/rsocket-grpc/index.html new file mode 100644 index 0000000..04f6545 --- /dev/null +++ b/post/rsocket-grpc/index.html @@ -0,0 +1,380 @@ + + + + + + + + Alternative RSocket-RPC: fast application services communication and transparent GRPC bridge + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +

    Alternative RSocket-RPC: fast application services communication and transparent GRPC bridge

    +
    May 20, 2021
    + +RSocket + +Mstreams + +java + +grpc + + +
    + +

    Exchanging millions of buffers per second with plain RSockets is not useful for business applications because +they need structured data instead of raw bytes.

    +

    That’s why there was RSocket-RPC - remote procedure call system on top of Protocol Buffers with +language agnostic service definitions. It uses code-generation for performance equivalent to hand-written code.

    +

    Protocol buffers have tiny on-wire overhead, acceptable performance and is native format of Grpc - important +for interoperability and available tooling that can be used directly, or easily adapted.

    +

    Alternative RSocket-RPC

    +

    Original RSocket/RSocket-RPC is based on problematic RSocket/RSocket-java +and seems is not supported anymore (generated RPC stubs do not compile). Approach based on code generation +and single efficient data format was dropped in favour of data abstraction and runtime +tricks to translate +between serialized and structured messages.

    +

    For alternative RSocket-RPC, code generation was redone to align It with jauntsdn/RSocket-reactor.

    +

    Let’s enumerate outstanding differences:

    +
      +
    • +

      Compact encoding of both RPC calls and user-provided metadata: in majority of cases RPC overhead is only 3-4 bytes.

      +
    • +
    • +

      Noticeably higher performance - result of improvements in jauntsdn/rsocket-reactor, +less pressure on memory allocator and compact metadata encoding: 1.3 million requests per second with request-response, +3 million messages per second with request-stream, per core.

      +
    • +
    • +

      Native compatibility with Grpc - if combined with respective transport. There is no need for separate binary or IDL +sharing schemes.

      +
    • +
    +

    GRPC transport

    +

    For internet clients, RSocket-RPC has limited utility because existing solutions (HTTP/Grpc) are mature and already +good enough. This relates to both client libraries and API gateways - haproxy, nginx and recently even AWS API gateway +support Grpc. Some of them have features and performance that would require years of dedicated teams to reach.

    +

    In data center environment, RSocket/RSocket-RPC has several advantages over Http2/Grpc, main of them is considerable performance gains.

    +

    Fortunately, RSocket streams are able to accommodate Http2 streams which then may be routed to RSocket-RPC origins over TCP.

    +

    This way RSocket-RPC services can be efficiently accessed by existing Grpc clients - the message format is same so there +is no penalty of cross-format data translation.

    +

    Scope and features:

    +
      +
    • +

      Server only transport - sufficient for external Grpc clients support.

      +
    • +
    • +

      Only client initiated requests are allowed - this is required by Http2 streams semantics. +Requests support request-response, server-stream, client-stream, bidistream interactions.

      +
    • +
    • +

      Assume API gateway/proxy between RSocket server and internet clients - no quic/http3 stack or message compression; +implies fast, reliable network for simpler HTTP2/RSocket flow control coordination.

      +
    • +
    +

    Like TCP and Unix sockets, GRPC transport can be used with any jauntsdn/RSocket-JVM +based implementation, and was verified with RSocket-reactor and RSocket-helidon.

    +

    It is lean dependency-wise: only io.netty:netty-codec-http2 and jauntsdn:rsocket-transport, +no Protobuf related libraries. Compiled JAR is less than 60 KBytes in size.

    +

    Performance

    +

    Performance was evaluated on commodity box running linux 5.4.0 with EPOLL IO and non-TLS TCP transport. +JVM is OpenJDK 11.0.11.

    +

    Its purpose is comparison against plain RSocket with RSocket-reactor on single core throughput.

    +

    This test gives estimate of per-request (with request-response) and per-message (request-stream, channel) RPC overhead.

    +

    Message size is selected small (less than 10 bytes) so most of CPU time is spent on RPC request +encoding & message serialization instead of memory copy, which is better gauge for RPC efficiency.

    +

    RSocket-RPC

    +
      +
    • REQUEST-STREAM
    • +
    +

    Single connection / single core stream throughput test.

    +
    16:55:09.484 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.stream.client.Main client received messages: 3094767
    +16:55:10.484 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.stream.client.Main client received messages: 2997843
    +16:55:11.484 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.stream.client.Main client received messages: 2996896
    +16:55:12.484 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.stream.client.Main client received messages: 3004344
    +16:55:13.484 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.stream.client.Main client received messages: 3000798
    +16:55:14.484 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.stream.client.Main client received messages: 3002806
    +

    3M msg/sec vs 3.5M msg/sec demonstrated by Plain RSocket, or 14% worse throughput.

    +

    This small overhead is caused by Protobuf serialization only as there is single continuous stream per connection +(for N response messages there is 1 request).

    +
      +
    • REQUEST-CHANNEL
    • +
    +

    Single connection / single core stream throughput test.

    +

    Server

    +
    16:56:20.876 rsocket-netty-io-transport-epoll-1-2 com.jauntsdn.rsocket.rpc.examples.channel.server.Main server received messages: 1726494
    +16:56:21.874 rsocket-netty-io-transport-epoll-1-2 com.jauntsdn.rsocket.rpc.examples.channel.server.Main server received messages: 1734271
    +16:56:22.877 rsocket-netty-io-transport-epoll-1-2 com.jauntsdn.rsocket.rpc.examples.channel.server.Main server received messages: 1734271
    +16:56:23.875 rsocket-netty-io-transport-epoll-1-2 com.jauntsdn.rsocket.rpc.examples.channel.server.Main server received messages: 1718717
    +16:56:24.875 rsocket-netty-io-transport-epoll-1-2 com.jauntsdn.rsocket.rpc.examples.channel.server.Main server received messages: 1710940
    +16:56:25.874 rsocket-netty-io-transport-epoll-1-2 com.jauntsdn.rsocket.rpc.examples.channel.server.Main server received messages: 1726494
    +

    Client

    +
    16:56:21.804 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.channel.client.Main client received messages: 1742048
    +16:56:22.804 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.channel.client.Main client received messages: 1726494
    +16:56:23.803 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.channel.client.Main client received messages: 1718717
    +16:56:24.804 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.channel.client.Main client received messages: 1718717
    +16:56:25.803 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.channel.client.Main client received messages: 1726494
    +16:56:26.803 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.channel.client.Main client received messages: 1734271
    +

    With request-channel RSocket-RPC exchanges ~3.4M msg/sec, while plain RSocket is at 4.8M msg/sec - 29% slower due +to additional serialization/deserialization of request stream (for N response messages there is N request messages).

    +
      +
    • REQUEST-RESPONSE
    • +
    +

    Single connection / single core continuous requests window throughput test.

    +
    16:58:16.609 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.response.client.Main client received messages: 1329611
    +16:58:17.607 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.response.client.Main client received messages: 1320154
    +16:58:18.608 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.response.client.Main client received messages: 1321358
    +16:58:19.609 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.response.client.Main client received messages: 1317221
    +16:58:20.607 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.response.client.Main client received messages: 1321927
    +16:58:21.607 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.response.client.Main client received messages: 1329611
    +

    RSocket-RPC demonstrates ~1.3M msg (requests)/sec, or 38% worse throughput compared to plain RSocket (2.1M requests / sec).

    +

    Substantial difference is due to additional overhead of request metadata encoding for each outbound message under request-response model +(for N responses there is N requests).

    +

    RSocket-GRPC

    +

    Test consists of RSocket-RPC server with RSocket-GRPC transport, and Grpc client based on grpc-java 1.37.0 over Netty, +under OpenJDK 11.0.11.

    +

    Services and messages are identical to ones from RSocket-RPC test.

    +

    Each result is compared with grpc-java-only setup.

    +
      +
    • SERVER-STREAM
    • +
    +

    Single stream throughput test.

    +
    17:52:03.752 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.stream.Main Client received messages: 1428347
    +17:52:04.752 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.stream.Main Client received messages: 1454406
    +17:52:05.752 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.stream.Main Client received messages: 1568482
    +17:52:06.752 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.stream.Main Client received messages: 1480556
    +17:52:07.752 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.stream.Main Client received messages: 1386296
    +17:52:08.752 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.stream.Main Client received messages: 1492142
    +

    Server-stream is 50% slower compared to RSocket-RPC over TCP: the reason is chattiness of Http2, overhead of grpc-java +on client side, netty http2 codec on server side.

    +

    On the other hand numbers correspond to the best case with grpc-java only setup which has distinctively more fluctuations +in 0.8-1.5M msg/sec range.

    +
      +
    • BIDI-STREAM
    • +
    +

    Single stream throughput test.

    +
    17:53:04.856 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.bidistream.Main ==> Bidi stream sent messages: 709548, received messages: 458496
    +17:53:05.856 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.bidistream.Main ==> Bidi stream sent messages: 790257, received messages: 291072
    +17:53:06.856 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.bidistream.Main ==> Bidi stream sent messages: 828460, received messages: 276224
    +17:53:07.856 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.bidistream.Main ==> Bidi stream sent messages: 831259, received messages: 275083
    +17:53:08.856 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.bidistream.Main ==> Bidi stream sent messages: 600732, received messages: 818881
    +17:53:09.856 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.bidistream.Main ==> Bidi stream sent messages: 664493, received messages: 656692
    +17:53:10.856 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.bidistream.Main ==> Bidi stream sent messages: 850823, received messages: 280517
    +

    Exchange is around 1.2M msg/sec, comparable to grpc-java only setup.

    +
      +
    • REQUEST-RESPONSE
    • +
    +

    Continuous requests window throughput test.

    +
    10:52:47.579 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.response.Main Client received messages: 101663
    +10:52:48.579 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.response.Main Client received messages: 103388
    +10:52:49.579 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.response.Main Client received messages: 101563
    +10:52:50.579 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.response.Main Client received messages: 101165
    +10:52:51.579 pool-2-thread-1 com.jauntsdn.rsocket.showcase.grpc.response.Main Client received messages: 101255
    +

    The result is around 100K msg (requests)/sec, same with grpc-java only setup.

    +

    Numbers do not look impressive compared to order of magnitude faster RSocket-RPC counterpart (1.3M requests/sec), +but this is what current Http2/Grpc-java/Netty stack is able to offer.

    +
    + + + + + + + +
    + + + + + + + + +

    RSocket-JVM: streamlining implementation for each vendor platform

    +
    April 22, 2021
    + +RSocket + +Mstreams + +java + + + +
    + + + + + + +
    + +
    + +
    + + + + + + + + diff --git a/post/rsocket-intro/index.html b/post/rsocket-intro/index.html new file mode 100644 index 0000000..dc96d15 --- /dev/null +++ b/post/rsocket-intro/index.html @@ -0,0 +1,268 @@ + + + + + + + + RSocket: short introduction + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +

    RSocket: short introduction

    +
    March 17, 2020
    + +RSocket + +Mstreams + + +
    + +

    Description from official RSocket-java repository

    +
    RSocket is a binary protocol for use on byte stream transports...
    +It enables the following symmetric interaction models via async message passing over a single connection..
    +

    leaves bits of mystery unless reader is interested enough to skim over lengthy FAQ and motivations documents. +This post is one page size summary of distinctive RSocket features that make It a good companion for Http2/GRPC.

    +

    Low latency

    +

    RSocket provides means for maintaining low latency under high throughput by limiting incoming and outgoing message queue +size on both sides of connection:

    +
      +
    • +

      Stream level flow control with Reactive Streams. +It guarantees that for single request, peer publisher will send no more messages than demanded with RequestN frame.

      +
    • +
    • +

      Connection level with requests Leasing. +It limits the number of concurrent streams on Responder with expiring allowed requests grant.

      +
    • +
    +

    Often disconnects are inherent for mobile clients. Latency adds up on each hop between content producer and consumer +due to cascade of service reconnects. This is addressed by automatic session Resumption enabled on edge servers and clients.

    +

    Both are transparent for application layer: because part of the protocol, services implemented on top +of RSocket are not concerned about existence of the above.

    +

    Any transport

    +

    RSocket is session layer protocol, transport is anything able to reliably send bytes in order: +from websockets to tcp to unix sockets.

    +

    Compatible with http2

    +

    RSocket streams are similar and naturally map to http2 streams. RSockets on top of such +transport can be routed by web gateways/proxies/loadbalancers.

    +

    Both typed and raw byte messages

    +

    RSocket-RPC is efficient Protocol Buffers / code generation based RPC system similar to GRPC. +It is intended for application services while proxies use byte stream RSockets directly for lower overhead.

    +

    Fits existing ecosystem

    +

    RSocket streams are semantically equivalent to core types of Reactive Streams compliant projects - +on JVM these are projectreactor, rxjava, akka-streams. They simplify protocol implementation, +and dependent applications benefit from available libraries and familiar programming model.

    +

    Afterword

    +

    RSocket is well balanced capabilities-wise. It has pragmatic scope: efficiently transfer byte streams across networks and processes, +delivered with small set of orthogonal features (reactive streams, request leasing, resumption). +It is easy to comprehend and implement. Moving any feature out would make RSocket not suitable for problems It was intended for. +Overloading specification with ad-hoc concepts - message layout, routing, tracing - puts It in odd middleground of lack of +focus and questionable interop still without richness expected from application level protocol.

    +
    + + + + + + + + + + + + + + +
    + +
    + +
    + + + + + + + + diff --git a/post/rsocket-jvm/index.html b/post/rsocket-jvm/index.html new file mode 100644 index 0000000..18ec3e8 --- /dev/null +++ b/post/rsocket-jvm/index.html @@ -0,0 +1,373 @@ + + + + + + + + RSocket-JVM: streamlining implementation for each vendor platform + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +

    RSocket-JVM: streamlining implementation for each vendor platform

    +
    April 22, 2021
    + +RSocket + +Mstreams + +java + + +
    + +

    Motivation

    +

    In practice reactive applications - as a method to model software and programming paradigm of composable flow controlled +streams - viable mainly within JVM context.

    +

    It has necessary ecosystem provided by several competing platforms: +smallrye-mutiny by Red Hat, +project reactor by (ex-)Pivotal, +and community-maintained rxjava.

    +

    Libraries are interoperable on same VM because their APIs semantically conform to reactive-streams +specification, with 3 of 4 having binary compatibility - streams may be combined directly without writing any adapter code.

    +

    On the other hand interprocess and network communication is done with Grpc and Http(/2).

    +

    Http is general purpose application protocol to retrieve resources from server to client with request/response model.

    +

    Grpc is also based on Http/2 streams, that’s why It is also non-symmetric (only client can initiate requests to server), and has +byte oriented flow control while Grpc encodes data as protobuf messages.

    +

    Stable Grpc stack is available for wide range of programming languages, however its java impl has “awkward” streaming APIs, +lacks composability and produces relatively high volume of per-message garbage.

    +

    Http/Grpc/Graphql is good choice for heterogeneous internet environment as general purpose solution, +with libraries and tools available for many hardware/os/language combinations.

    +

    For specialized cloud environment which happens to be JVM centric there is another option - RSocket. +It is binary, symmetric, session level protocol with multiple streaming interactions support, reactive-streams message +flow control and pluggable transports.

    +

    Initially there was official rsocket/rsocket-java implementation, based on project-reactor.

    +

    Unfortunately all its core features, transports and instrumentation modules are based on +spring-boot related dependencies that can’t be reused on competing platforms.

    +

    Porting rsocket/rsocket-java to each platform would be not only laborious, but also unfeasible effort given +questionable performance of rsocket/rsocket-java post 1.0 release.

    +

    Instead protocol core was reimplemented with minimal, vendor-neutral dependencies so each +vendor project can be built upon It - rsocket-jvm.

    +

    This way next library development becomes predictable and largely mechanical process - most of the functionality is shared, +while general structure becomes clear once first implementation is completed.

    +

    rsocket-jvm was evaluated with 4 vendor libraries: project-reactor, helidon-reactive-streams, rxjava & smallrye-mutiny.

    +

    Structure

    +

    Conceptually all components belong to 5 categories:

    +
      +
    • API CONTRACTS.
    • +
    +

    RSocket-messages is netty-buffers only project, accompanied by RSocket-<VENDOR>, RSocket-RPC-<VENDOR> pair for +each vendor library (see RSocket-reactor, RSocket-RPC-reactor for example).

    +

    API contracts plus RSocket-RPC-compiler are sufficient for implementing end-user services without exposing unnecessary runtime details.

    +
      +
    • TRANSPORT CONTRACT.
    • +
    +

    Includes type declarations for transports - components able to send and receive raw bytes. They only depend on rsocket-messages +so transports may be used with each platform library.

    +
      +
    • PROTOCOL COMPONENTS.
    • +
    +

    Contain software building blocks implementing major features of the protocol: handshakes, keep-alive, +requests leasing, session resumption, server graceful close, error handling etc.
    +Additionally It hosts APIs (but not implementations) that enable end-user application specific code sharing +across platforms: RSocket interceptors and connection listeners for metrics, requests lease handlers for load estimation, +resumable session stores and session discovery.

    +
      +
    • SHARED MODULES.
    • +
    +

    Denote projects utilized by each platform library as ready-to-use components, both +public and private. Currently these are TCP, Unix sockets & GRPC transports, load estimator and metrics.

    +
      +
    • PLATFORM LIBRARY.
    • +
    +

    Top-level component intended for end users. It contains RSocket, RSocket-RPC implementations (plus RPC compiler) with vendor +specific APIs and glue code to link protocol components together. +Platform library is based on Protocol components and is configurable with Transport and Shared modules.

    +

    RSocket-JVM

    +

    Transport and Shared components comprise core of the project that is foundational for each platform library.

    +

    rsocket-jvm assumes Java 8+ and depends on netty-buffer only.

    +

    RSocket - Reactor

    +

    Chronologically is first project that emerged as result of researching for 1 million streams (part2).

    +

    It is based on project-reactor of spring boot framework.

    +

    Comparing to original rsocket/rsocket-java, rsocket-reactor consumes 2x less CPU for same workloads, with virtually non-existent +per message garbage.

    +

    Numbers below are from runs on commodity box with linux 5.4.0 and TCP/EPOLL, OpenJDK 11.0.11.

    +

    Single core request-response throughput, messages per second

    +
    08:34:32.638 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.response.client.Main client received messages: 2153067
    +08:34:33.638 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.response.client.Main client received messages: 2142966
    +08:34:34.638 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.response.client.Main client received messages: 2144672
    +08:34:35.638 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.response.client.Main client received messages: 2153692
    +08:34:36.638 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.response.client.Main client received messages: 2137679
    +

    Single core server-stream throughput, messages per second

    +
    08:39:17.839 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.stream.client.Main client received messages: 3567742
    +08:39:18.839 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.stream.client.Main client received messages: 3575242
    +08:39:19.839 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.stream.client.Main client received messages: 3577742
    +08:39:20.839 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.stream.client.Main client received messages: 3587117
    +08:39:21.839 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.stream.client.Main client received messages: 3577742
    +

    Single core channel throughput, messages per second

    +

    Client

    +
    08:39:01.915 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.channel.client.Main client received messages: 2333310
    +08:39:02.938 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.channel.client.Main client received messages: 2411087
    +08:39:03.915 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.channel.client.Main client received messages: 2333310
    +08:39:04.923 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.channel.client.Main client received messages: 2411087
    +08:39:05.943 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.channel.client.Main client received messages: 2411087
    +

    Server

    +
    08:39:00.984 rsocket-netty-io-transport-epoll-1-2 com.jauntsdn.rsocket.examples.channel.server.Main server received messages: 2411087
    +08:39:01.979 rsocket-netty-io-transport-epoll-1-2 com.jauntsdn.rsocket.examples.channel.server.Main server received messages: 2333310
    +08:39:02.969 rsocket-netty-io-transport-epoll-1-2 com.jauntsdn.rsocket.examples.channel.server.Main server received messages: 2333310
    +08:39:03.978 rsocket-netty-io-transport-epoll-1-2 com.jauntsdn.rsocket.examples.channel.server.Main server received messages: 2411087
    +08:39:04.986 rsocket-netty-io-transport-epoll-1-2 com.jauntsdn.rsocket.examples.channel.server.Main server received messages: 2411087
    +

    RSocket-Helidon

    +

    Project is based on reactive-streams-common from helidon se - toolkit +for rapid development of cloud java applications.

    +

    reactive-streams-common is JDK only reactive library using Flow from java.util.concurrent instead of reactive-streams-jvm, +and has tight integration with JDK’s CompletableFutures.

    +

    It has relatively modest set of operators available out of the box, but custom ones are easy to implement.

    +

    Initial evaluation with TCP transport demonstrated that rsocket-helidon performs slightly better than rsocket-reactor. +At the same time helidon-common-reactive has considerably simpler internals compared to project-reactor.

    +

    Numbers below are from runs on commodity box with linux 5.4.0 and TCP/EPOLL, running OpenJDK 11.0.11.

    +

    Single core request-response throughput, messages per second

    +
    08:45:11.301 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.response.client.Main client received messages: 2574258
    +08:45:12.305 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.response.client.Main client received messages: 2418647
    +08:45:13.301 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.response.client.Main client received messages: 2317546
    +08:45:14.292 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.response.client.Main client received messages: 2301992
    +08:45:15.290 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.response.client.Main client received messages: 2317546
    +08:45:16.302 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.response.client.Main client received messages: 2356431
    +

    Single core server-stream throughput, messages per second

    +
    08:54:05.186 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.stream.client.Main client received messages: 3757323
    +08:54:06.186 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.stream.client.Main client received messages: 3767671
    +08:54:07.186 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.stream.client.Main client received messages: 3762671
    +08:54:08.186 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.stream.client.Main client received messages: 3758573
    +08:54:09.186 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.stream.client.Main client received messages: 3765171
    +

    Single core channel throughput, messages per second

    +

    Client

    +
    08:53:33.035 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.channel.client.Main client received messages: 2566641
    +08:53:34.016 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.channel.client.Main client received messages: 2488864
    +08:53:35.027 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.channel.client.Main client received messages: 2566641
    +08:53:36.008 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.channel.client.Main client received messages: 2566641
    +08:53:37.012 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.channel.client.Main client received messages: 2644418
    +

    Server

    +
    08:53:30.025 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.channel.client.Main client received messages: 2488864
    +08:53:31.013 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.channel.client.Main client received messages: 2488864
    +08:53:32.023 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.channel.client.Main client received messages: 2566641
    +08:53:33.035 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.channel.client.Main client received messages: 2566641
    +08:53:34.016 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.examples.channel.client.Main client received messages: 2488864
    +

    Server host CPU usage while running 1 million streams load test as described in first part of 1 million streams:

    +

    RSocket-helidon

    +
     PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND  
    +7435 maksym    20   0   15,1g   2,3g  28192 S 452,3   7,5  12:00.72 java  
    +

    RSocket-reactor

    +
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND  
    +6384 maksym    20   0   15,0g   2,6g  28088 S 538,9   8,4  13:02.96 java  
    +

    On this workload helidon-reactive-streams consumes 20% less CPU time compared to project-reactor.

    +

    RSocket-rxjava, RSocket-smallrye-mutiny

    +

    Both libraries have internals similar to helidon-commons-reactive, so the performance is rather close.

    +

    Examples

    +

    rsocket-jvm-interop-examples is 4 vendor libraries +interop demonstration (runtimes are stripped for now).

    +
    + + + + + + + + + + + + + + +
    + +
    + +
    + + + + + + + + diff --git a/post/rsocket-lease-concurrency-limiting/index.html b/post/rsocket-lease-concurrency-limiting/index.html new file mode 100644 index 0000000..486bcee --- /dev/null +++ b/post/rsocket-lease-concurrency-limiting/index.html @@ -0,0 +1,451 @@ + + + + + + + + Service concurrency limiting with RSocket request leases + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +

    Service concurrency limiting with RSocket request leases

    +
    April 9, 2020
    + +RSocket + +Mstreams + +java + +load-balancer + + +
    + +
    +

    This is first post in advanced applications of RSocket series, and topic for today is less known yet essential +protocol feature - requests leasing.

    +

    We know that single RSocket stream is naturally bounded due to Reactive Streams semantics - there is no more +in-flight messages than demanded with receiver Subscription.

    +

    However limiting outstanding messages per request is not useful without limiting requests concurrency.

    +

    For services, growing requests unbounded exhausts their backing resources and leads to latency surge.

    +

    For proxies, messages queue up on sender and receiver side, network link hits capacity limit - both lead to +gradual memory & latency increase.

    +

    Relying solely on latency aware load balancer on Requester side is not enough. Historical stats are +not meaningful if service is about to be overwhelmed by requests spike and latencies grow exponentially. +This event makes service unusable, all in-flight requests are affected and likely to timeout.

    +

    Requester side load balancer needs to be paired with Responder side mechanism ensuring requests volume does +not exceed capacity of the service and It can serve responses in a timely manner.

    +

    RSocket provides this concurrency limiting mechanism with Requests Lease. It is based on simple idea
    +of expiring request permits sent periodically to its peer by Responder.

    +

    How It works

    +

    Lease rejection policy is implemented on Responder side only, requests over leased capacity are short-circuited +before hitting request handler. Rejecting on requester side is not performed because Responder should be +aware of actual requests volume.

    +

    Requester RSocket does not expose leased requests directly, instead It provides availability as remaining requests / leased requests +ratio. Expired lease has availability 0.0.

    +

    Lease is primarily intended for server side use cases, notably for reverse proxies in front of services set.
    +In simplest case proxy selects service RSocket with highest availability for next request thus implements least-loaded +balancing strategy, solely Responder side driven. It works well in homogeneous networks since services can reliably +estimate own capacity for given target latency, and aggressively drop what is above.

    +

    This contrasts with least-loaded LB applied alone on requester side, which would produce suboptimal results as It effectively treats +service instances as having same capacity, even if backed by different hardware.

    +

    Service Responder gathers stats to estimate allowed requests: number of received/completed requests, +response errors and response latencies aggregated over logical service call name. For response streams latency is defined as +interval between request is received and first signal of response is produced.

    +

    Proxies may use round-trip time as latency estimate, It is provided by RSocket keep-alive frame which is able to carry data payload. +RTT includes time spent on network and incoming / outgoing queues of both peers. Responder allowed requests should be decreased +once RTT substantially exceeds expected latency value.

    +

    (Bit of trivia) Official RSocket-java repo started treating keep-alive frames as +prioritized, so RTT will only include time spent on network and not on RSocket queues. There under some unfortunate circumstances +one could witness few milliseconds round trips but requests dropped by multiple second timeouts.

    +

    Building blocks

    +

    Request leasing is enabled on RSocketFactory for both server and client of jauntsdn/rsocket.

    +
    RSocketFactory.lease(Lease.Configurer);
    +
    +interface Configurer {
    +
    +    Optional<Lease.StatsRecorder<?>> configure(Lease.Controller leaseController);
    +  }
    +

    Lease.Configurer is a handle for 2 components:

    +
      +
    • Lease.StatsRecorder is for gathering requests statistics
    • +
    • Lease.Controller is for allowing requests leases sent to peer Requester, and used by Responder.
    • +
    +
    interface Controller {
    +   
    +    void allow(int timeToLiveMillis, int allowedRequests); 
    +}
    +

    StatsRecorder<T> contains set of callbacks tied to lifecycle of request and response on Responder side. <T> parameter +denotes logical name of the call, e.g. for RSocket-RPC this can be String of form service/method, and is provided in every callback.

    +
    interface StatsRecorder<T> {
    +
    +    T onRequestStarted(Interaction.Type requestType, ByteBuf metadata);
    +
    +    void onResponseStarted(
    +            Interaction.Type requestType,
    +            T request,
    +            Interaction.StreamSignal firstSignal,
    +            long latencyMicros);
    +
    +    void onResponseTerminated(
    +            Interaction.Type requestType,
    +            T request,
    +            Interaction.StreamSignal lastSignal,
    +            long responseDurationMicros);
    +    
    +    void onRtt(long rttMicros);
    +   
    +    void onOpen();
    +    
    +    void onClose(long graceTimeoutMillis);
    +    
    +    void onError(Interaction.Type requestType, Throwable err);
    +  }
    +
      +
    • +

      onRequestStarted is called once Responder receives the request, +returns logical name of the call based on request type and metadata.

      +
    • +
    • +

      void onResponseStarted is called +once response first signal is received (either data or termination). latencyMicros is interval between start of the request +and first signal of the response.

      +
    • +
    • +

      void onResponseTerminated is called once response is terminated.

      +
    • +
    • +

      void onRtt is called on round-trip time measured with keep-alive frame.

      +
    • +
    +

    LeaseController is handed to StatsRecorder, which uses stats +to estimate allowed requests and expiration time. Produced leases are sent to peer requester, and utilized by +Responder to implement lease policy - rejected requests are terminated with RSocket REJECTED error containing either +lease_expired or lease_exhausted message.

    +

    On requester RSocket allowed requests are expressed by RSocket.availability() as ratio between remaining and initially allowed +requests. For example, if requester receives lease of 10 requests over 1000 millis, initial availability is 10/10 = 1.0. +Once one request is sent, availability is 9/10 = 0.9. Expired lease has availability 0.0.

    +

    Showcase

    +

    Lets compose demo application to evaluate feature in practice.

    +

    It consists of three parts: set of servers hosting simple RSocket-RPC service, reverse proxy fronting servers and RSocket-RPC client. +Leasing is enabled on servers and proxy, and disabled on client.

    +

    Service is implemented as saturable one, with response times proportional to number of (concurrent) requests in given time window. +It roughly models real-world service, either IO bound - backed by database, or CPU bound involving heavy computations.

    +

    Response time delays are controlled with CONCURRENCY_DELAY property containing string of form 10 => 2; 50 => 5; 120 => 20; => 5000. +It means that for concurrency less than 10, latency will be no more than 2 millis; for concurrency 11 - 50 latency is 2-5 millis; +for concurrency 51 - 120 latency is 5 - 20 millis, anything above will be served with 20 - 5000 millis delay.

    +

    Stats are gathered by ServiceStatsRecorder which records number of accepted and rejected requests, latencies per RPC call. +Leases are produced by StaticLeaseSender which allows constant number of requests (ALLOWED_REQUESTS property) every second.

    +

    Both are wired up on ServerRSocketFactory:

    +
        Lease.Configurer leases =
    +        leaseController -> {
    +          ConstantLeaseController constantLeaseController =
    +              new ConstantLeaseController(
    +                  leaseController,
    +                  inetSocketAddress,
    +                  leaseTimeToLive,
    +                  leaseAllowedRequests);
    +          return Optional.of(constantLeaseController);
    +        };
    +
    +RSocketFactory
    +     .receive()
    +     .lease(leases)
    +

    Proxy relies on LeastLoadedBalancerRSocket connecting to backend servers provided with SERVERS property. Property contains +set of addresses in form localhost:8309,localhost:8310,localhost:8311. LeastLoadedBalancerRSocket selects RSocket with highest +availability to serve next request, and switches to round-robin once all RSockets availability is exhausted.

    +

    Client connects proxy and sends 1000 requests per second to RSocket-RPC service.

    +

    Unbounded leased requests: 9999

    +

    We start testing with effectively unbounded allowed requests (9999) on 3 services: localhost:8309,8310,8311

    +
    ./lease_server.sh localhost:8309 9999
    +./lease_server.sh localhost:8310 9999
    +./lease_server.sh localhost:8311 9999
    +

    Proxy

    +
    ./lease_proxy.sh localhost:8308 localhost:8309,localhost:8310,localhost:8311
    +

    Client

    +
    `./lease_client.sh localhost:8308`
    +

    On client we see there are no rejected requests as each of 3 services provide 9999 allowed requests that is more than enough to fullfill client’s 1000 rps. +Response p99 latency is expectedly subpar - around 5000ms.

    +
    15:16:26.068 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client ================================================================================
    +15:16:26.068 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client Responses from localhost:8310: 322
    +15:16:26.068 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client Responses from localhost:8309: 317
    +15:16:26.068 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client Responses from localhost:8311: 335
    +15:16:26.068 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client Responses p99 latency millis: 4877
    +15:16:26.069 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client Rejected requests: 0
    +

    Similar stats are reported by services themselves

    +
    15:17:47.101 reactor-tcp-epoll-2 com.jauntsdn.rsocket.lease.showcase.Server service localhost:8311 accepted 329 requests
    +15:17:47.101 reactor-tcp-epoll-2 com.jauntsdn.rsocket.lease.showcase.Server service call Service/response latency is 4898 millis
    +15:17:47.101 reactor-tcp-epoll-2 com.jauntsdn.rsocket.lease.showcase.Server responder sends new lease, allowed requests is 9999, time-to-live is 1000 millis
    +

    Moderate leased requests: 100

    +

    On client we see there are 300 accepted and 700 rejected requests. Latency is just around 20 millis.

    +
    15:25:44.430 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client ================================================================================
    +15:25:44.430 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client Responses from localhost:8309: 100
    +15:25:44.430 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client Responses from localhost:8310: 100
    +15:25:44.430 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client Responses from localhost:8311: 100
    +15:25:44.430 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client Responses p99 latency millis: 19
    +15:25:44.430 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client Rejected requests: 700
    +

    Service stats correspond client numbers: 100 accepted requests was leased by each service, 233 rejected and same latency - 20 millis.

    +
    15:25:44.087 reactor-tcp-epoll-2 com.jauntsdn.rsocket.lease.showcase.Server ================================================================================
    +15:25:44.087 reactor-tcp-epoll-2 com.jauntsdn.rsocket.lease.showcase.Server service localhost:8310 accepted 100 requests
    +15:25:44.087 reactor-tcp-epoll-2 com.jauntsdn.rsocket.lease.showcase.Server service localhost:8310 rejected 233 requests
    +15:25:44.087 reactor-tcp-epoll-2 com.jauntsdn.rsocket.lease.showcase.Server service call Service/response latency is 19 millis
    +15:25:44.087 reactor-tcp-epoll-2 com.jauntsdn.rsocket.lease.showcase.Server responder sends new lease, allowed requests is 100, time-to-live is 1000 millis
    +

    Small leased requests: 30

    +

    Now client reports acceptable latency of 4 millis with 90 accepted and 910 rejected requests

    +
    15:32:00.926 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client ================================================================================
    +15:32:00.926 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client Responses from localhost:8310: 30
    +15:32:00.926 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client Responses from localhost:8309: 30
    +15:32:00.926 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client Responses from localhost:8311: 30
    +15:32:00.927 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client Responses p99 latency millis: 4
    +15:32:00.927 parallel-1 com.jauntsdn.rsocket.lease.showcase.Client Rejected requests: 910
    +

    Services stats correlate

    +
    15:33:13.725 reactor-tcp-epoll-2 com.jauntsdn.rsocket.lease.showcase.Server ================================================================================
    +15:33:13.725 reactor-tcp-epoll-2 com.jauntsdn.rsocket.lease.showcase.Server service localhost:8310 accepted 30 requests
    +15:33:13.725 reactor-tcp-epoll-2 com.jauntsdn.rsocket.lease.showcase.Server service localhost:8310 rejected 304 requests
    +15:33:13.725 reactor-tcp-epoll-2 com.jauntsdn.rsocket.lease.showcase.Server service call Service/response latency is 4 millis
    +15:33:13.725 reactor-tcp-epoll-2 com.jauntsdn.rsocket.lease.showcase.Server responder sends new lease, allowed requests is 30, time-to-live is 1000 millis
    +

    Results are summarized below:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Leased requests   Response latency   Accepted requests   Rejected requests   
    999948779740
    10019300700
    30490910
    +

     
    +RSocket requests leasing is central protocol feature which enables concurrency and latency control without guesswork on caller side. +It helps applications remain stable and can greatly reduce timeout related errors. It is transparent for RSocket services - there are no leasing related references in +service API definitions or implementations. Static leases sender can be substituted with smarter one that estimates allowed requests based on +Responder statistics and target service call latency. Algorithms are known and described at concurrency-limits - +we will see that in action soon reported on the pages of this blog.

    +
    + + + + + + + + + + + + + + +
    + +
    + +
    + + + + + + + + diff --git a/post/rsocket-million-streams-1/index.html b/post/rsocket-million-streams-1/index.html new file mode 100644 index 0000000..92a57a9 --- /dev/null +++ b/post/rsocket-million-streams-1/index.html @@ -0,0 +1,384 @@ + + + + + + + + Serving one million streams. Part 1. RSocket limits on the JVM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +

    Serving one million streams. Part 1. RSocket limits on the JVM

    +
    June 24, 2020
    + +RSocket + +Mstreams + +java + + +
    + +

    One of the interesting topics is estimation of RSocket limits in setup that approximates gateway serving tens of thousands clients.

    +

    There are numerous reports focusing on few streams over single connection, with emphasis on huge throughput. +However this mode is not intrinsical for real server. Even though gateways are more likely to be cpu bound than IO bound, +we can relax this assumption by making server logic trivial (just echo back incoming messages), to see how RSocket +behaves under idealized conditions when major resource consumption is related to IO.

    +

    We use jauntsdn/rsocket to have http2 based transports, +and metrics that are actually useful (RSocket/RSocket-java metrics do not distinguish inbound/outbound messages, +generate redundant time series and seems were not designed with efficient querying in mind - at least for Prometheus case. +Also numerous vulnerabilities (1 , +2, +3, 4, +5) +that were not resolved despite it’s post 1.0.0 GA hint that library is not +yet intended for internet facing services - use case we are interested in here).

    +

    Goals

    +
      +
    • Multiple transports: tcp, websocket, http2, websocket-over-http2.
    • +
    • Runs both locally and in data center (aws), ops/automation friendly.
    • +
    • Distributed load clients.
    • +
    • Keep-It-simple with test configuration options for load clients: +number of connections per instance, number of streams per connection, ramp up interval.
    • +
    • Test only long-running request-stream interactions - dominating model for clients +connecting over the internet - browsers & mobile applications.
    • +
    +

    Software

    +

    Software stack is comprised of stable & widely adopted components:

    +

    Docker for building client/server images & running containers. +Test application images are created & saved to configured docker registry as part of project build process.
    +Nomad for containers scheduling with built-in docker driver.
    +Consul for service discovery. We register 2 services for load server instance

    +
      +
    • rsocket-stress-test-server for RSocket load itself.
    • +
    • rsocket-stress-test-server-metrics for http server exposing /metrics endpoint scraped by Prometheus.
    • +
    +

    Prometheus stores host, JVM & server metrics scraped from rsocket-stress-test-server-metrics endpoint.
    +Grafana does metrics visualizations used to evaluate performance.

    +

    Load server contains RSocket server parameterized with network/IO transports and security configurations. +Responder request-stream echoes back batches of original message at given intervals (effectively 1 message per second).

    +

    Load clients are configured with number of connections/streams, ramp up interval. They start new +streams monotonically until target count is reached. Request message contains random bytes, and is 1KB in size.

    +

    Both clients and servers use G1GC with default settings.

    +

    The application can be deployed locally, running load test with local infrastructure +is a matter of changing few urls:

    +
    export DOCKER_REPOSITORY_ADDRESS=localhost:5000
    +export CONSUL_ADDRESS=localhost:8500
    +export NOMAD_ADDR=http://localhost:4646
    +

    Hardware

    +

    Server is equivalent of AWS c4.2xlarge - mainstream VM with 8 vCPU / 16Gb RAM / 1000MBit network.

    +

    Clients are c4.xlarge having 4 vCPU / 8Gb RAM / 1000MBit network each.

    +

    Comparable resources may be allocated on common developer’s box that helps with local experiments.

    +

    Test cases

    +

    Test application consists of 2 load clients connecting single load server instance.

    +

    Each load client is configured with env variables:

    +
    export CONNECTIONS=5000
    +export STREAMS=25
    +export RAMP_UP_PERIOD=250 #seconds 
    +

    This results in 125k simultaneous streams per client, 250k streams per server over 10k connections.

    +

    Startup script generates Nomad’s job specification files from templates by substituting
    +configuration env variables, then asks Nomad to schedule test application.

    +

    First test is for TCP transport, configured for 250k, 500k, 750k and 1000k streams respectively.

    +

    We deliberately omit latency measurements until Part 2 - the goal is to determine maximum load before server starts to tip over.

    +

    Tcp, 250K simultaneous streams

    +

    250k streams over 10k connections, 125 seconds ramp up.

    +
    ==> Client # of connections: 5000
    +==> Client # of streams: 25
    +==> Client ramp up period, seconds: 125
    +
    +==> Starting server using scripts/server.nomad...
    +
    +==> Monitoring evaluation "3b158360"
    +    Evaluation triggered by job "rsocket-stress-test-server"
    +    Allocation "2eae67ce" created: node "b4fd7677", group "servers"
    +    Evaluation within deployment: "69514dd5"
    +    Allocation "2eae67ce" status changed: "pending" -> "running" (Tasks are running)
    +    Evaluation status changed: "pending" -> "complete"
    +==> Evaluation "3b158360" finished with status "complete"
    +
    +==> Waiting 15s until server is started...
    +
    +==> Starting clients using scripts/client.nomad...
    +
    +==> Monitoring evaluation "00b0c5e7"
    +    Evaluation triggered by job "rsocket-stress-test-client"
    +    Allocation "38387f71" created: node "b4fd7677", group "clients"
    +    Allocation "453a9469" created: node "b4fd7677", group "clients"
    +    Evaluation within deployment: "4e7c3c74"
    +    Allocation "38387f71" status changed: "pending" -> "running" (Tasks are running)
    +    Allocation "453a9469" status changed: "pending" -> "running" (Tasks are running)
    +    Evaluation status changed: "pending" -> "complete"
    +==> Evaluation "00b0c5e7" finished with status "complete"
    +
    +==> Stress test started
    +==> It will be stopped automatically after 1h
    +==> Can be stopped manually with ./test_stop.sh
    +

    The base case has passed with moderate cpu usage: ~ 25%, total memory used peak (heap + non-heap) around 6GB.
    +

    + +
    +
    +
    + +
    +
    +
    + +
    +

    +

    Tcp, 500K simultaneous streams

    +

    500k streams over 10k connections, 250 seconds ramp up.

    +

    Cpu usage grows linearly with streams count: peaked at 50% for cpu, and same level of total memory peak: 6GB.

    +

    + +
    +
    +
    + +
    +
    +
    + +
    +

    +

    Tcp, 750K simultaneous streams

    +

    750k streams over 10k connections, 375 seconds ramp up.

    +

    Test demonstrates the server starts to tip over around 600k streams, 7 GB memory, 60% process cpu - but this time at 100% host cpu usage. +The result is an artifact of scheduling both server and clients on same host, and actual streams peak is higher - +around 700-750k will consume 70-75% cpu - practical limit for stable service operation.

    +

    The number is promising as there are few opportunities for optimizations - as will be shown in part 2 of the post.

    +

    + +
    +
    +
    + +
    +
    +
    + +
    +

    +

    Interesting observation - allocation rate peaks at 800MB/s even though load server application code does not allocate +directly. Handful of objects instantiated by library per message are significant contribution to total GC pressure once +number of messages becomes millions.
    +

    + +
    +

    +

    GC pauses of this run, for reference +

    + +
    +

    +

    Part 2

    +

    In second part we will continue with design decisions review +of the original library, their impact on performance and how It can be improved.

    +
    + + + + + + + + + + + + + + +
    + +
    + +
    + + + + + + + + diff --git a/post/rsocket-million-streams-2/index.html b/post/rsocket-million-streams-2/index.html new file mode 100644 index 0000000..88ec0fb --- /dev/null +++ b/post/rsocket-million-streams-2/index.html @@ -0,0 +1,384 @@ + + + + + + + + Serving one million streams. Part 2. Double performance over original RSocket library + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +

    Serving one million streams. Part 2. Double performance over original RSocket library

    +
    January 20, 2021
    + +RSocket + +Mstreams + +java + + +
    + +

    New year break is good time for continuing on topic of one million of streams - +serving huge amount of RSocket streams simultaneously with single, mid-level commodity computer.

    +

    The original library gave in around 500k streams - mark where server was still stable, at whopping 500-600 MBytes/s allocation rate - unexpected +numbers for RSockets just sending same byte buffer periodically, using library advertising zero-copy capability.

    +

    Interesting question is whether we hit natural limit, caused by current state of JVM runtime and libraries, or is a consequence +of design choices driven at large by “ideological” and marketing motives +that eventually resulted in implementation having significant parts happen to exist +just for burning CPU cycles - both directly and through garbage collection?

    +

    Let’s start with the numbers: performance comparison of original rsocket/rsocket-java library +versus jauntsdn/rsocket-java redesigned with pragmatics-first approach.

    +

    Numbers

    +

    Tests were performed on the same hardware described in first part +of report, using TCP for transport. Host box is 16 vCPU / 32 gb RAM; 8vCPU/16gb is allocated for server, +and 4vCPU/8gb - for each of 2 clients.

    +

    500k

    +

    500k streams over 5k connections; graphs show server active streams and CPU usage, respectively

    +

    + +
    +
    +
    + +
    +

    +

    Server based on redesigned library consumes 25% CPU vs 50% CPU original (part 1: TCP, 500K simultaneous streams), yields 2x improvement.

    +

    1000k

    +

    1000k streams over 10k connections; graphs show server active streams and CPU usage, respectively

    +

    + +
    +
    +
    + +
    +

    +

    Test server is stable while handling 1 million of simultaneous streams +at 60% CPU usage (and 80% host CPU, which also includes 2 clients) - value is comparable to 50% CPU with +original library at only 500k streams.

    +

    Graphs demonstrate that 1 million streams goal is met with new implementation while consistently using 2 times less CPU.

    +

    Let’s take a look at server heap allocation rates.

    +
    + +
    + +

    Allocation rate peaked at only 50 MBytes/s - quite an improvement over 800 MBytes/s (just on ~600k-700k streams) +of original library.

    +

    Given that streams interval is 1 sec, this corresponds to 50 MBytes allocated on heap per 1 million messages, +or at most 50 bytes per message sent.

    +

    The cost of reactive

    +

    Rxjava/project-reactor style libraries are useful because they enable lock-free asynchronous message processing +with user-friendly, fluent “functional”-style interface, are composable and offer good throughput-latency tradeoff.

    +

    This makes them a good choice for user-facing APIs on network applications. However, It does not necessarily +hold true for internal components underneath API surface - fluent style and composability are not free.

    +

    Some operators, e.g. flatMap +contain non-trivial logic and do 2 CAS updates per message, others (groupBy) +additionally exchange each message via SPSC-queue. On loaded servers with tens of thousands streams +It is common to see 10% of total CPU time spent inside flatMap itself!

    +

    Reactive everywhere

    +

    Original library rsocket/rsocket-java followed reactive dogma little too rigorously, and DuplexConnection - +its main transport interface, internal component for sending and receiving frames - exposes APIs as Publishers chained +with said flatMap and groupBy for processing inbound and outbound messages.

    +

    This dependency also forces every transport to implement reactor-netty adapters for a single purpose of sending/receiving raw bytes, +even though reactor-netty primary use case is HTTP.

    +

    Let’s get into details.

    +

    Reactive type in send method DuplexConnection.send(frames) +is redundant - It accepts Publisher<ByteBuf> of frames instead of simple synchronous method:

    +
    void send(ByteBuf frame);
    +

    Reactive type in receive method DuplexConnection.receive() +is redundant - It returns Flux<ByteBuf> instead of method with callback:

    +
    void receive(OnReceive onReceive);
    +
    +interface OnReceive {
    +
    +    void receive(ByteBuf frame);
    +}
    +

    RSocket itself is lightweight protocol and does not require asynchronous processing of messages, hence Publisher instead +of synchronous function is unnecessary complication.

    +

    One can argue Publisher on DuplexConnection.send()/receive() are for flow control, but there is upper bound +for pending messages already, provided by protocol itself: outstanding streams are limited with request leases, +and outstanding stream messages - by reactive-streams semantics of RSocket.

    +

    Odd assumptions

    +

    RSocket is intended as foundation for messaging systems, and they are designed as single-producer systems. +Multi-producer designs are expensive because at scale of million+ messages managing contention takes unacceptable +amount of CPU time compared to message processing itself.

    +

    RSocket-java relies on Netty as backbone for its frame transport implementations.

    +

    Netty is go-to messaging/networking toolkit on JVM, and It follows event loop pattern - inbound messages +of one connection are delivered on same thread - single producer. On outbound pipeline most of Netty components +also aware of event loop so writes have low overhead.

    +

    However there is no concept of event loop in original rsocket/RSocket-java - outbound frames are exchanged via +MPMC queue plus 2 CAS writes, +even if every outbound frame is on event loop thread.

    +

    This is not hypothetical situation - proxy/loadbalancer applications naturally have both inbound and outbound messages +produced on same thread.

    +

    Accidental inefficiency

    +

    Inbound and outbound streams state is stored in a map with all methods declared synchronized.

    +

    It is 2 ways problematic - first inbound frames apriori can’t race because they are published on event loop hence +no locking needed - still noncontended locks are not free. Then this adds contention between outbound request frames +and inbound frames for no particular reason. Code that appears like highly optimized for throughput +lives next to locks on a hot path.

    +

    Because DuplexConnection.receive() returns Flux<ByteBuf> of incoming frames, part of processing logic is +implemented as chain of operators inside ClientServerInputMultiplexer.

    +

    Unfortunately It does so with aforementioned flatMap, groupBy and another Processor having +good old 2 CAS operations and MPMC queue exchange. +This chain of operators is replaceable by plain function call.

    +

    Frame properties are read from memory multiple times for each message: streamId, frameType, flags, data & +metadata - instead of reading once and pass down callstack.

    +

    Also there is needless allocation for each outbound frame with payload caused by CompositeByteBuf.

    +

    Complexity

    +

    reactor-netty is main networking library in Spring/reactive ecosystem. Spring framework core ideas are flexibility +and productivity - virtually any component is pluggable, many components are available out of the box.

    +

    reactor-netty follows same line in spirit as new features and configuration options are added to project on first user request.

    +

    This leaves an impression of unbounded scope - reactor-netty is not just thin reactor-core adapter +around Netty’s channels.

    +

    Internal structure wise, It has layers of functions-calling-functions of Monos/Fluxes, +and other clever constructs manifesting in screen-sized call-stacks - net contributing only to wasted CPU cycles and +allocation rates.

    +

    Usability wise, there is unfortunate decision to combine reference-counted offheap memory with async streams of +reactor-project in user-facing APIs. Decision became burden on its users - who now are required detailed knowledge +of reactive-streams rules, project-reactor and reactor-netty primitives lifecycle to not leak native memory +while doing common client/server tasks.

    +

    Redesign

    +

    The goal of rework was addressing problems outlined above, let’s enumerate main points:

    +
      +
    • +

      Adopt straightforward design across library where every component exists for good reason; +external dependencies are lean and justified.

      +
    • +
    • +

      Replace reactive types with simple function calls / callbacks on all transports.

      +
    • +
    • +

      Drop reactor-netty from transport, use Netty directly since It gives fine-grained control over connection bytestreams, +removes the need to workaround reactive “ballast” that gives no value in transport context, plus leaner dependencies +that are more stable, easier to understand and maintain.

      +
    • +
    • +

      Simplify hot paths of sending and receiving frames: header attributes are calculated once, +drop necessity for CompositeByteBuffers, drop ClientServerInputMultiplexer - manifestation of +operators abuse is also replaced by a single plain function.

      +
    • +
    • +

      RSocket inbound streams - bridges from plain callbacks to reactive interactions(request-stream, request-channel etc) - +were reimplemented assuming event loop execution model with single producer.

      +
    • +
    • +

      Drop locks on a hot path of accessing streams maps - this is consequence of Netty event loop awareness in both streams +and transport.

      +
    • +
    +

    Conclusion

    +

    Keep-it-simple approach, well articulated scope, lean dependencies are necessary components for software infrastructure +library.

    +

    If principles are not respected, library ends up comprised from more-than-necessary components +each having hardly noticeable deficiencies, once stacked together contribute considerable amount to CPU waste.

    +

    In this particular case waste was 50% - and is directly reflected in hardware costs of each application on top of It.

    +
    + + + + + + + + + + + + + + +
    + +
    + +
    + + + + + + + + diff --git a/post/rsocket-summary/index.html b/post/rsocket-summary/index.html new file mode 100644 index 0000000..bfc1ef5 --- /dev/null +++ b/post/rsocket-summary/index.html @@ -0,0 +1,356 @@ + + + + + + + + 📌 Summary: alternative RSocket library for high performance network applications on JVM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +

    📌 Summary: alternative RSocket library for high performance network applications on JVM

    +
    February 1, 2023
    + +RSocket + +Mstreams + +java + + +
    + +

    Summary

    +
      +
    • lean & very fast GRPC-like services on JVM with rich streaming models over multiple data-center and internet transports.
    • +
    • multiple APIs: CompletableFuture & virtual threads; traditional streaming GRPC-stub (StreamObserver), or reactive with smallrye-mutiny, rxjava, reactor
    • +
    • pluggable networking: tcp, unix sockets, VM sockets; grpc, websockets, websockets-over-http2
    • +
    • service APIs / codegen stubs (Message-Streams) are split from library runtime (RSocket-JVM, including network transports, load estimators, load balancers, metrics)
    • +
    • transparent origin (RPC) & proxy load estimation for low-cost yet efficient load balancers
    • +
    • native image support with graalvm
    • +
    +

    Project consists of API modules for each vendor library - Message-Streams, and respective runtime implementations - RSocket-JVM.

    +

    Message-Streams includes RPC: code-generation based remote procedure call system on top of Protocol Buffers.

    +

    Message-Streams sources are available on github, and binaries are +published on MavenCentral.

    +

    RSocket is low latency/high throughput L5 network protocol intended for high-performance services communication. +It is transport agnostic and may run on top of any reliable byte stream transport.

    +

    Fast alternative RSocket on JVM

    +

    Several problems were present in official RSocket/RSocket-java library:

    +
      +
    • low performance and excessive garbage production, as described in 1 million streams. part2. +Official RSocket/RSocket-java/RSocket-RPC performed similar to Http2/netty based GPRC, and did not scale on big hosts.
    • +
    • no reusable code for other vendor implementations: core features, transports, metrics were coded with Pivotal libraries +(e.g. project-reactor, reactor-netty and micrometer).
    • +
    • vulnerabilities outlined in 1 million streams. part1, +recently continued with 1, +2, +3, +4 - 3 of 4 interactions (all except fire-and-forget) may +be used for trivial denial-of-service of RSocket/RSocket-java servers.
    • +
    • RSocket/RSocket-RPC abandoned in favor of “Spring way” of data format abstraction, and reflection at runtime instead of code generation.
    • +
    +

    Message Streams with flow control

    +

    Protocol offers rich set of interactions modelled as composable streams of binary messages, with flow control, error handling and cancellation as first-class concepts:

    +

    fire-and-forget (1:0), request-response (1:1), request-stream (1:n), request-channel (n:n).

    +

    Interactions are exposed to end user with MessageStreams API.

    +

    Traditional streaming

    +

    GRPC-stubs (StreamObserver) API support allows drop-in replacement of GRPC-java in services for significantly better throughput.

    +
    interface MessageStreams {
    +  void requestResponse(Message message, StreamObserver<Message> responseObserver);
    +  void requestStream(Message message, StreamObserver<Message> responseObserver);
    +  StreamObserver<Message> requestChannel(StreamObserver<Message> responseObserver);
    +  void fireAndForget(Message message, StreamObserver<Message> responseObserver);
    +}  
    +

    Reactive streams

    +

    Reactive Streams conformance makes protocol compatible with project-reactor, smallrye-mutiny, rxjava3, and helidon-commons-reactive

    +
    interface MessageStreams {
    +  Publisher<Message> requestResponse(Message message);
    +  Publisher<Message> requestStream(Message message);
    +  Publisher<Message> requestChannel(Publisher<Message> messages);
    +  Publisher<Void> fireAndForget(Message message);
    +}
    +

    This library demonstrates throughput of up to millions of messages per second / per vCPU with every interaction - more details below.

    +

    RSocket-JVM

    +

    Library relies on shared protocol core with minimal dependencies (only netty-buffer) for use with +vendor libraries on JVM.

    +

    RSocket-JVM is currently comprised of JDK only RSocket-futures for request-response (CompletableFuture), traditional streaming with RSocket-GRPC (GRPC-stub), several flavors of reactive: RSocket-rxjava (rxjava3), RSocket-reactor (project-reactor) and RSocket-mutiny (smallrye-mutiny).

    +
    + +
    + +

    Notable features:

    +
      +
    • substantially higher performance, tiny per message garbage production. It is capable of serving 1 million of simultaneous +streams with mid-range commodity PC (1 million streams.part2).
    • +
    +
             message size,bytes    8(same payload)        8             128            512
    +
    +request-response                     2.2m            1.7m           1.6m          1.3m
    +request-stream                       3.5m            3.2m           3.2m          3.2m
    +request-channel                      4.8m            3.2m           3.1m          2.4m
    +

    Table 1. Single vCPU RSocket throughput (project-reactor, non-tls, TCP EPOLL with linux 5.4.0, jdk11), +millions of messages per second.

    +
      +
    • requests leasing for circuit breaking, adaptive latency-based load estimation that enables efficient +lightweight load balancers.
    • +
    • request ranks concept, used by load estimator to assist with load balancing
    • +
    • graceful close of both clients and server, scalable server keep-alive support, +lightweight coarse scheduler.
    • +
    • built-in metrics model reworked so only tiny, practically useful set of metrics is published.
    • +
    +

    GRPC compatibility

    +

    jauntsdn/RSocket and jauntsdn/RSocket-RPC is compatible with GRPC: protocol design allows its streams to accommodate http2 streams. +Compatibility was verified with RSocket-GRPC transport, observed performance characteristics were identical +to official grpc-java only setup.

    +

    RPC for application services

    +

    jauntsdn/RSocket-RPC is RPC system based on Message-Streams & Protocol Buffers. It relies on code generation with protobuf C++ plugin extensions and is compatible with GRPC.

    +

    Notable features:

    +
      +
    • optimal off-heap buffers utilization, compact rpc call encoding, default service concept for small request on-wire overhead
    • +
    • request ranks, which are used by load estimator and assist with load balancing
    • +
    • idempotent requests
    • +
    • simple metadata in form of ASCII encoded key-value pairs (headers)
    • +
    • instrumentation with no binary dependency on Micrometer or OpenTracing
    • +
    +

    The result is significantly higher performance: millions of messages per second per core, with each interaction.

    +
             message size,bytes          8             128            512
    +
    +request-response                    1.45m          1.0m          0.55m
    +request-stream                      3.3m           2.4m          0.9m
    +request-channel                     3.5m           2.4m          1.25m
    +

    Table 2. Single vCPU RSocket-RPC throughput (project-reactor, non-tls, TCP EPOLL with linux 5.4.0, jdk11), +millions of messages per second.

    +

    Multiple network transports

    +

    RSocket is transport agnostic, and runs on top of any reliable byte stream transport. +jauntsdn/RSocket project hosts several transports, shared & directly usable by each RSocket-JVM vendor specific library:

    +
      +
    • +

      TCP, unix domain sockets transports: known efficient byte stream protocols for datacenter / inter-process communication.

      +
    • +
    • +

      Http based transports for interop:

      + +
    • +
    +

    Load estimation

    +

    Network/service latency based load estimation for RSocket & RSocket-RPC with requests leasing, +which enables low-cost yet efficient load balancers.

    +

    Source code/binaries

    +

    Message-Streams (RSocket-JVM API) sources are available on github, and binaries are +published on MavenCentral.

    +
    + + + + + + + +
    + + + + + + + + +

    RSocket-JVM: streamlining implementation for each vendor platform

    +
    April 22, 2021
    + +RSocket + +Mstreams + +java + + + +
    + + + + + + +
    + +
    + +
    + + + + + + + + diff --git a/post/rsocket-vs-spring/index.html b/post/rsocket-vs-spring/index.html new file mode 100644 index 0000000..b570ec3 --- /dev/null +++ b/post/rsocket-vs-spring/index.html @@ -0,0 +1,379 @@ + + + + + + + + Jaunt-RSocket-RPC, Spring-RSocket, GRPC: quantitative and qualitative comparison + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +

    Jaunt-RSocket-RPC, Spring-RSocket, GRPC: quantitative and qualitative comparison

    +
    September 3, 2021
    + +RSocket + +Mstreams + +java + + +
    + +

    Earlier this year we described weaknesses of Spring RSocket-java +implementation that resulted in awful throughput, latency & garbage production: key metrics for core networking library.

    +

    Latest Spring-RSocket uses RSocket/RSocket-java version 1.1.1, and wraps up almost a year of progress made on the library.

    +

    The goal of this post is to re-evaluate that progress against both current jauntsdn/RSocket-RPC (not yet publicly available), +and grpc/grpc-java.

    +

    Jauntsdn-RSocket-RPC vs Spring-RSocket-CBOR: quantitative comparison

    +

    jauntsdn/RSocket-RPC is remote procedure call system using streaming libraries (GRPC-API or several reactive - including RSocket-reactor) +for networking & APIs, and Protocol Buffers as sole data format. It relies on code generation with protobuf C++ plugin extensions, and is compatible with GRPC.

    +

    Spring-RSocket (version 2.5.3) offers reflection-heavy client and server on top of RSocket-java. +Both are configured with HttpMessageEncoder and HttpMessageDecoder from spring’s http stack. Each call starts with +interpolated “route” template string, followed by message encoding/decoding operators chain (for client chain size varies from 7 for +request-response to 14 for request-channel).

    +

    Out of 2 serialization options suggested in their docs we choose CBOR instead of JSON as more compact so theoretically faster.

    +

    Additionally GRPC-java (1.37) only setup is used as performance base line. Both client and server are configured with +static flow control window of 1_000_000.

    +

    Our objective is quick performance re-evaluation since 1 million streams. part2, +so only single vCPU throughput is measured for 2 common interactions: request-stream (single infinite stream) and request-response +(continuous window of requests).

    +

    Payload sizes are selected as follows:
    +4 (practical minimum: packed telemetry), 140 (tweet in latin) bytes

    +

    Tests were conducted on 12 vCPU commodity box running linux 5.4.0/OpenJDK 11.0.11, with non-TLS TCP transport.

    +

    Request-stream

    +
      +
    • PACKED TELEMETRY MESSAGE (4 BYTES)
    • +
    +

    jauntsdn/RSocket-RPC

    +
    11:33:17.378 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.stream.client.Main client received messages: 3485145
    +11:33:18.378 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.stream.client.Main client received messages: 3458668
    +11:33:19.378 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.stream.client.Main client received messages: 3468615
    +

    spring-RSocket-CBOR

    +
    2021-09-03 12:15:12.766  INFO 11603 --- [     parallel-2] s.r.stream.client.ClientApplication      : client received messages: 548060
    +2021-09-03 12:15:13.766  INFO 11603 --- [     parallel-2] s.r.stream.client.ClientApplication      : client received messages: 541163
    +2021-09-03 12:15:14.766  INFO 11603 --- [     parallel-2] s.r.stream.client.ClientApplication      : client received messages: 522535
    +

    GRPC-Java

    +
    12:32:16.273 pool-2-thread-1 com.jauntsdn.grpc.stress_test.stream.Main  client received messages: 1667608
    +12:32:17.273 pool-2-thread-1 com.jauntsdn.grpc.stress_test.stream.Main  client received messages: 1687596
    +12:32:18.273 pool-2-thread-1 com.jauntsdn.grpc.stress_test.stream.Main  client received messages: 1657052
    +
      +
    • TWEET MESSAGE (140 BYTES)
    • +
    +

    jauntsdn/RSocket-RPC

    +
    12:21:18.751 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.stream.client.Main client received messages: 2519878
    +12:21:19.751 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.stream.client.Main client received messages: 2426705
    +12:21:20.737 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.stream.client.Main client received messages: 2426704
    +

    spring-RSocket-CBOR

    +
    2021-09-03 12:20:06.531  INFO 12357 --- [     parallel-2] s.r.stream.client.ClientApplication      : client received messages: 497637
    +2021-09-03 12:20:07.531  INFO 12357 --- [     parallel-2] s.r.stream.client.ClientApplication      : client received messages: 493875
    +2021-09-03 12:20:08.531  INFO 12357 --- [     parallel-2] s.r.stream.client.ClientApplication      : client received messages: 497627
    +

    GRPC-Java

    +
    12:36:51.114 pool-2-thread-1 com.jauntsdn.grpc.stress_test.stream.Main  client received messages: 1004188
    +12:36:52.114 pool-2-thread-1 com.jauntsdn.grpc.stress_test.stream.Main  client received messages: 1026791
    +12:36:53.114 pool-2-thread-1 com.jauntsdn.grpc.stress_test.stream.Main  client received messages: 988619
    +

    With request-streams spring-RSocket not only 6x slower (3.4M vs 0.55M) than jauntsdn-RSocket-RPC, but 2-3x slower than GRPC-java itself - +“legacy” library It was aiming to supersede.

    +

    Request-response

    +
      +
    • PACKED TELEMETRY MESSAGE (4 BYTES)
    • +
    +

    jauntsdn/RSocket-RPC

    +
    12:23:41.381 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.response.client.Main client received messages: 1489591
    +12:23:42.383 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.response.client.Main client received messages: 1504273
    +12:23:43.382 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.response.client.Main client received messages: 1493177
    +

    spring-RSocket-CBOR

    +
    2021-09-03 12:23:49.258  INFO 12745 --- [     parallel-2] s.r.response.client.ClientApplication    : client received messages: 101036
    +2021-09-03 12:23:50.258  INFO 12745 --- [     parallel-2] s.r.response.client.ClientApplication    : client received messages: 102121
    +2021-09-03 12:23:51.258  INFO 12745 --- [     parallel-2] s.r.response.client.ClientApplication    : client received messages: 102134
    +

    GRPC-Java

    +
    12:38:15.146 pool-2-thread-1 com.jauntsdn.grpc.stress_test.response.Main client received messages: 89277
    +12:38:16.146 pool-2-thread-1 com.jauntsdn.grpc.stress_test.response.Main client received messages: 91057
    +12:38:17.146 pool-2-thread-1 com.jauntsdn.grpc.stress_test.response.Main client received messages: 91510
    +
      +
    • TWEET MESSAGE (140 BYTES)
    • +
    +

    jauntsdn/RSocket-RPC

    +
    12:24:34.828 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.response.client.Main client received messages: 1072298
    +12:24:35.829 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.response.client.Main client received messages: 1065523
    +12:24:36.828 rsocket-netty-io-transport-epoll-1-1 com.jauntsdn.rsocket.rpc.examples.response.client.Main client received messages: 1058572
    +

    spring-RSocket-CBOR

    +
    2021-09-03 12:25:13.025  INFO 13023 --- [     parallel-2] s.r.response.client.ClientApplication    : client received messages: 93324
    +2021-09-03 12:25:14.025  INFO 13023 --- [     parallel-2] s.r.response.client.ClientApplication    : client received messages: 92235
    +2021-09-03 12:25:15.025  INFO 13023 --- [     parallel-2] s.r.response.client.ClientApplication    : client received messages: 94413
    +

    GRPC-Java

    +
    12:43:11.855 pool-2-thread-1 com.jauntsdn.grpc.stress_test.response.Main client received messages: 85854
    +12:43:12.855 pool-2-thread-1 com.jauntsdn.grpc.stress_test.response.Main client received messages: 82733
    +12:43:13.855 pool-2-thread-1 com.jauntsdn.grpc.stress_test.response.Main client received messages: 82587
    +

    On request-response situation turned bleak very quickly for spring-RSocket: 10x-14x slower (1.4M vs 0.1M) than jauntsdn-RSocket-RPC. +I didn’t believe the measurements at first, so had to double check - setup was correct.

    +

    On the bright side, spring-RSocket holds on par against GRPC - with one digit percent advantage.

    +

    Let’s look at these results from practical point - with current Spring, 80% (request-stream) to 90% (request-response) of CPU time is wasted +processing RSocket/RSocket-java & framework internals - share of the cloud infra money that are turned into heat on datacenter hardware.

    +

    Vulnerabilities

    +

    Several recent cases 1, 2, +3, 4 demonstrate that latest Spring RSocket/RSocket-java servers are still subject to +trivial denial-of-service attacks, and look like implemented for happy case only.

    +

    3 interactions (request-response, request-stream, request-channel) of 4 may be used for server memory overflow, +plus one case of server file descriptors exhaustion.

    +

    They are similar in spirit to those reported last year for general-availability version of RSocket/RSocket-java +(enumerated at top of the post).

    +

    Spring-RSocket: false promise

    +

    With Spring and RSocket/RSocket-java, specialized binary, purpose-built cloud L5 protocol has glaringly worse performance +over TCP than prior existing GRPC-java over http2 - general purpose internet protocol.

    +

    It leaves false impression of RSocket-as-protocol deficiency, while in reality It solely reflects engineering capacity, +goals & decisions of organization behind Spring & RSocket/RSocket-java.

    +

    RSocket-as-protocol is capable of times better throughput with very small latency/GC pressure compared to GRPC - +if proper implementation is in place.

    +

    When coupled with Reactive-Streams library (at least 3 of production ready quality available for JVM), RSocket enables +bidirectional exchange of non-blocking message streams across processes (unix sockets) and networks (tcp, websockets, http2, udp/quic), +at rate of millions messages per core, with few milliseconds latency.

    +

    Spring’s RSocket state is caused by questionable foundation of RSocket/RSocket-java, bloated metadata encoding, +switching from approach of single data format & codegen to data format abstraction plus reflection - which cannot +be efficient or optimized due to substantial difference between data formats.

    +

    Note that both jauntsdn/RSocket-RPC-reactor and Spring RSocket/RSocket-java use same non-blocking primitives +provided by netty & project-reactor, but have 5x-10x throughput difference - textbook example on consequences +if proper design, scope & conservative dependencies are neglected.

    +

    Unification vs specialization

    +

    Unification vs specialization is a tradeoff: in particular for data, with former It is not possible to have +“universal” optimizations, effective on multiple considerably different data formats - performance is traded +for pluggability.

    +

    As enterprise applications framework Spring follows unification paradigm from its inception, so every piece of It +is designed pluggable - at the expense of efficiency.

    +

    Unfortunately, as soon as server applications started being deployed to thoroughly metered 3rd party cloud VMs, +efficiency and resources usage became primary concern.

    +

    That’s why jauntsdn/RSocket-RPC specializes on single data format - Protocol Buffers. It is compact, fast, widely +supported and native for GRPC.

    +

    In case of spring-RSocket substituting codec with protobuf did not increase throughput, and also did not make It directly +accessible by GRPC clients, despite proven RSocket-GRPC streams interoperability.

    +

    Jauntsdn-RSocket-RPC vs GRPC: qualitative comparison

    +

    Both jauntsdn/RSocket-RPC and GRPC are based on Protocol Buffers (protobuf). They rely on protobuf compiler’s service extensions +framework to generate client & server stubs from language agnostic service and message definitions.

    +

    GRPC service APIs are callbacks based, while jauntsdn/RSocket-RPC APIs are Publisher based: which means they are composable, +and represent errors and cancellations as first class concept.

    +

    Performance evaluation of RSocket-RPC was presented earlier, +let’s continue with short list of qualitative differences between the two:

    +
      +
    • jauntsdn/RSocket-RPC has indisputable performance advantage in key metrics: throughput, latency (request/message level backpressure), +garbage production
    • +
    • jauntsdn/RSocket-RPC understands GRPC, but not vise versa
    • +
    • jauntsdn/RSocket-RPC is compatible with web browsers (if combined with websocket or websocket-over-http2 transport)
    • +
    • jauntsdn/RSocket-RPC is symmetric - both client (connection initiator) and server may start requests, while with GRPC is asymmetric: +only client may start requests.
    • +
    • jauntsdn/RSocket-RPC client handshake may have payload containing arbitrary data, while with GRPC this data must be duplicated for each +request.
    • +
    • jauntsdn/RSocket-RPC may open multiple RSockets per connection with http2 (websocket-over-http2) transport, +while GRPC-java supports at most 1 channel per connection.
    • +
    • jauntsdn/RSocket-RPC can use any reliable byte transport: TCP, Unix Sockets, quic/http3 etc, while GRPC is http2 only
    • +
    • jauntsdn/RSocket-RPC may be transparently relayed by existing API gateways/proxies if backed by IETF standard protocol: +e.g. websocket or websocket-over-http2, while GRPC is proprietary protocol on top of http2 framing.
    • +
    • jauntsdn/RSocket-RPC uses same data format as GRPC - Protocol buffers, so existing libraries & tools are available +from Protobuf ecosystem.
    • +
    +
    + + + + + + + +
    + + + + + + + + +

    RSocket-JVM: streamlining implementation for each vendor platform

    +
    April 22, 2021
    + +RSocket + +Mstreams + +java + + + +
    + + + + + + +
    + +
    + +
    + + + + + + + + diff --git a/projects/index.html b/projects/index.html new file mode 100644 index 0000000..67026a3 --- /dev/null +++ b/projects/index.html @@ -0,0 +1,78 @@ + + + + + + + + + + + + Message-streams + + + + + +
    + +
    +
    Projects
    + +
    Collection of general purpose libraries used to implement RSocket-JVM, designed mainly with throughput/latency performance in mind. +
    + +
    MESSAGE STREAMS / RSOCKET-JVM. Very fast GRPC-like & GRPC-compatible + services on JVM with rich streaming models over multiple network transports. [1]. + Millions of requests/messages per core with each interaction model, million of simultaneous streams served with commodity PC.
    +
    NETTY-WEBSOCKET-HTTP1. Alternative Netty implementation of RFC6455 - the WebSocket protocol. + [2]. Its advantage is significant per-core throughput improvement (1.8 - 2x) for small frames in comparison to netty's out-of-the-box websocket + codecs, and minimal heap allocations on frame path. Compatible with netty-websocket-http2.
    +
    NETTY-WEBSOCKET-HTTP2. Netty based implementation of rfc8441 - bootstrapping websockets with http/2 + [3] + . Library addresses 2 use cases: for application servers and clients, It is transparent use of existing http1 websocket handlers on top of http2 streams; for gateways/proxies, + It is websockets-over-http2 support with no http1 dependencies and minimal overhead. Compatible with netty-websocket-http1.
    +
    NETTY-VMSOCKET. Implementation of Netty channels for VM sockets (aka VSOCK address family) [4].
    + +
    +
    + + + + diff --git a/projects/index.xml b/projects/index.xml new file mode 100644 index 0000000..37f5e1d --- /dev/null +++ b/projects/index.xml @@ -0,0 +1,14 @@ + + + + JAUNT.SDN + https://jauntsdn.com/projects/ + Recent content on JAUNT.SDN + Hugo -- gohugo.io + en-us + + + + + + \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..1fe9892 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,134 @@ + + + + + https://jauntsdn.com/ + 2023-02-01T10:18:00+02:00 + + + + https://jauntsdn.com/tags/java/ + 2023-02-01T10:18:00+02:00 + + + + https://jauntsdn.com/tags/mstreams/ + 2023-02-01T10:18:00+02:00 + + + + https://jauntsdn.com/post/ + 2023-02-01T10:18:00+02:00 + + + + https://jauntsdn.com/tags/rsocket/ + 2023-02-01T10:18:00+02:00 + + + + https://jauntsdn.com/tags/ + 2023-02-01T10:18:00+02:00 + + + + https://jauntsdn.com/post/rsocket-summary/ + 2023-02-01T10:18:00+02:00 + + + + https://jauntsdn.com/post/rsocket-vs-spring/ + 2021-09-03T10:18:00+02:00 + + + + https://jauntsdn.com/post/rsocket-grpc/ + 2021-05-20T08:00:00+02:00 + + + + https://jauntsdn.com/tags/grpc/ + 2021-05-20T08:00:00+02:00 + + + + https://jauntsdn.com/post/rsocket-jvm/ + 2021-04-22T08:00:00+02:00 + + + + https://jauntsdn.com/post/rsocket-million-streams-2/ + 2021-01-20T10:00:00+02:00 + + + + https://jauntsdn.com/tags/http2/ + 2020-07-30T11:00:00+02:00 + + + + https://jauntsdn.com/tags/netty/ + 2020-07-30T11:00:00+02:00 + + + + https://jauntsdn.com/tags/websocket/ + 2020-07-30T11:00:00+02:00 + + + + https://jauntsdn.com/post/netty-websocket-http2/ + 2020-07-30T11:00:00+02:00 + + + + https://jauntsdn.com/post/rsocket-million-streams-1/ + 2020-06-24T19:30:00+02:00 + + + + https://jauntsdn.com/tags/load-balancer/ + 2020-04-09T17:00:00+02:00 + + + + https://jauntsdn.com/post/rsocket-lease-concurrency-limiting/ + 2020-04-09T17:00:00+02:00 + + + + https://jauntsdn.com/post/rsocket-intro/ + 2020-03-17T11:30:00+02:00 + + + + https://jauntsdn.com/tags/io_uring/ + 2019-03-02T08:00:00+02:00 + + + + https://jauntsdn.com/post/rpc-iouring/ + 2019-03-02T08:00:00+02:00 + + + + https://jauntsdn.com/mstreams/ + 0 + + + + https://jauntsdn.com/mstreams/presentation/plugin/notes/speaker-view/ + 0 + + + + https://jauntsdn.com/projects/ + 0 + + + + https://jauntsdn.com/categories/ + + + \ No newline at end of file diff --git a/tags/grpc/index.html b/tags/grpc/index.html new file mode 100644 index 0000000..2af1f14 --- /dev/null +++ b/tags/grpc/index.html @@ -0,0 +1,173 @@ + + + + + + + + grpc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    grpc

    + + + + +
    + + + +
    + +
    + + + + + + + + diff --git a/tags/grpc/index.xml b/tags/grpc/index.xml new file mode 100644 index 0000000..f7d66ab --- /dev/null +++ b/tags/grpc/index.xml @@ -0,0 +1,26 @@ + + + + grpc on JAUNT.SDN + https://jauntsdn.com/tags/grpc/ + Recent content in grpc on JAUNT.SDN + Hugo -- gohugo.io + en-us + Thu, 20 May 2021 08:00:00 +0200 + + + + + + Alternative RSocket-RPC: fast application services communication and transparent GRPC bridge + https://jauntsdn.com/post/rsocket-grpc/ + Thu, 20 May 2021 08:00:00 +0200 + + https://jauntsdn.com/post/rsocket-grpc/ + Exchanging millions of buffers per second with plain RSockets is not useful for business applications because they need structured data instead of raw bytes. +That&rsquo;s why there was RSocket-RPC - remote procedure call system on top of Protocol Buffers with language agnostic service definitions. It uses code-generation for performance equivalent to hand-written code. +Protocol buffers have tiny on-wire overhead, acceptable performance and is native format of Grpc - important for interoperability and available tooling that can be used directly, or easily adapted. + + + + \ No newline at end of file diff --git a/tags/grpc/page/1/index.html b/tags/grpc/page/1/index.html new file mode 100644 index 0000000..3a170bd --- /dev/null +++ b/tags/grpc/page/1/index.html @@ -0,0 +1 @@ +https://jauntsdn.com/tags/grpc/ \ No newline at end of file diff --git a/tags/helidon/index.html b/tags/helidon/index.html new file mode 100644 index 0000000..daa20d5 --- /dev/null +++ b/tags/helidon/index.html @@ -0,0 +1,179 @@ + + + + + + + + helidon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    helidon

    + + + + +
    + + + +
    + +
    + + + + + + + + diff --git a/tags/helidon/index.xml b/tags/helidon/index.xml new file mode 100644 index 0000000..dd84f53 --- /dev/null +++ b/tags/helidon/index.xml @@ -0,0 +1,26 @@ + + + + helidon on JAUNT.SDN + https://jauntsdn.com/tags/helidon/ + Recent content in helidon on JAUNT.SDN + Hugo -- gohugo.io + en-us + Thu, 22 Apr 2021 08:00:00 +0200 + + + + + + RSocket-JVM: streamlining implementation for each vendor platform + https://jauntsdn.com/post/rsocket-jvm/ + Thu, 22 Apr 2021 08:00:00 +0200 + + https://jauntsdn.com/post/rsocket-jvm/ + Motivation In practice reactive applications - as a method to model software and programming paradigm of composable flow controlled streams - viable mainly within JVM context. +It has necessary ecosystem provided by several competing platforms: reactive-streams-common from helidon se by Oracle, akka-streams from akka by Lightbend, project reactor from spring boot by Pivotal, and standalone community-maintained rxjava. +Libraries are interoperable on same VM because their APIs semantically conform to reactive-streams specification, with 3 of 4 having binary compatibility - streams may be combined directly without writing any adapter code. + + + + \ No newline at end of file diff --git a/tags/helidon/page/1/index.html b/tags/helidon/page/1/index.html new file mode 100644 index 0000000..d354c51 --- /dev/null +++ b/tags/helidon/page/1/index.html @@ -0,0 +1 @@ +https://jauntsdn.com/tags/helidon/ \ No newline at end of file diff --git a/tags/http2/index.html b/tags/http2/index.html new file mode 100644 index 0000000..18f4dad --- /dev/null +++ b/tags/http2/index.html @@ -0,0 +1,173 @@ + + + + + + + + http2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    http2

    + +
    + + + + + + + + +

    WebSockets over http/2: implementing RFC8441 with Netty

    +
    July 30, 2020
    + +netty + +websocket + +http2 + +java + + + +
    + + +
    + + + +
    + +
    + + + + + + + + diff --git a/tags/http2/index.xml b/tags/http2/index.xml new file mode 100644 index 0000000..c38fd49 --- /dev/null +++ b/tags/http2/index.xml @@ -0,0 +1,27 @@ + + + + http2 on JAUNT.SDN + https://jauntsdn.com/tags/http2/ + Recent content in http2 on JAUNT.SDN + Hugo -- gohugo.io + en-us + Thu, 30 Jul 2020 11:00:00 +0200 + + + + + + WebSockets over http/2: implementing RFC8441 with Netty + https://jauntsdn.com/post/netty-websocket-http2/ + Thu, 30 Jul 2020 11:00:00 +0200 + + https://jauntsdn.com/post/netty-websocket-http2/ + Today I&rsquo;d like to introduce you to netty-websocket-http2 - implementation of websockets-over-http2, first release of which is about to land on the Maven Central. +Novel protocol graduated out of draft phase in September 2018, and is one of the smallest RFCs I&rsquo;ve ever seen - just 4 pages of actual specification. +Despite tiny size It opens some interesting possibilities - for both clients and servers. +Protocol defines Extended CONNECT Method - mechanism for establishing byte stream tunnel over single http2 stream, and addresses biggest flaw of websocket over http1 - need for a separate tcp connection per websocket. + + + + \ No newline at end of file diff --git a/tags/http2/page/1/index.html b/tags/http2/page/1/index.html new file mode 100644 index 0000000..fb8445f --- /dev/null +++ b/tags/http2/page/1/index.html @@ -0,0 +1 @@ +https://jauntsdn.com/tags/http2/ \ No newline at end of file diff --git a/tags/index.html b/tags/index.html new file mode 100644 index 0000000..919a6d1 --- /dev/null +++ b/tags/index.html @@ -0,0 +1,239 @@ + + + + + + + + Tags + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    Tags

    + +
    + + + + + + + + +

    java

    +
    February 1, 2023
    + + + +
    +
    + + + + + + + + +

    Mstreams

    +
    February 1, 2023
    + + + +
    +
    + + + + + + + + +

    RSocket

    +
    February 1, 2023
    + + + +
    +
    + + + + + + + + +

    grpc

    +
    May 20, 2021
    + + + +
    +
    + + + + + + + + +

    http2

    +
    July 30, 2020
    + + + +
    + + +
    + + + +
    + + + + + + + +
    + + + +
    + +
    + + + + + + + + diff --git a/tags/index.xml b/tags/index.xml new file mode 100644 index 0000000..5583270 --- /dev/null +++ b/tags/index.xml @@ -0,0 +1,96 @@ + + + + Tags on JAUNT.SDN + https://jauntsdn.com/tags/ + Recent content in Tags on JAUNT.SDN + Hugo -- gohugo.io + en-us + Wed, 01 Feb 2023 10:18:00 +0200 + + + + + + java + https://jauntsdn.com/tags/java/ + Wed, 01 Feb 2023 10:18:00 +0200 + + https://jauntsdn.com/tags/java/ + + + + + Mstreams + https://jauntsdn.com/tags/mstreams/ + Wed, 01 Feb 2023 10:18:00 +0200 + + https://jauntsdn.com/tags/mstreams/ + + + + + RSocket + https://jauntsdn.com/tags/rsocket/ + Wed, 01 Feb 2023 10:18:00 +0200 + + https://jauntsdn.com/tags/rsocket/ + + + + + grpc + https://jauntsdn.com/tags/grpc/ + Thu, 20 May 2021 08:00:00 +0200 + + https://jauntsdn.com/tags/grpc/ + + + + + http2 + https://jauntsdn.com/tags/http2/ + Thu, 30 Jul 2020 11:00:00 +0200 + + https://jauntsdn.com/tags/http2/ + + + + + netty + https://jauntsdn.com/tags/netty/ + Thu, 30 Jul 2020 11:00:00 +0200 + + https://jauntsdn.com/tags/netty/ + + + + + websocket + https://jauntsdn.com/tags/websocket/ + Thu, 30 Jul 2020 11:00:00 +0200 + + https://jauntsdn.com/tags/websocket/ + + + + + load-balancer + https://jauntsdn.com/tags/load-balancer/ + Thu, 09 Apr 2020 17:00:00 +0200 + + https://jauntsdn.com/tags/load-balancer/ + + + + + io_uring + https://jauntsdn.com/tags/io_uring/ + Sat, 02 Mar 2019 08:00:00 +0200 + + https://jauntsdn.com/tags/io_uring/ + + + + + \ No newline at end of file diff --git a/tags/io_uring/index.html b/tags/io_uring/index.html new file mode 100644 index 0000000..931320b --- /dev/null +++ b/tags/io_uring/index.html @@ -0,0 +1,169 @@ + + + + + + + + io_uring + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    io_uring

    + +
    + + + + + + + + +

    IO_URING for Netty based RPC: quick analysis

    +
    March 2, 2019
    + +io_uring + +netty + + + +
    + + +
    + + + +
    + +
    + + + + + + + + diff --git a/tags/io_uring/index.xml b/tags/io_uring/index.xml new file mode 100644 index 0000000..be594b7 --- /dev/null +++ b/tags/io_uring/index.xml @@ -0,0 +1,27 @@ + + + + io_uring on JAUNT.SDN + https://jauntsdn.com/tags/io_uring/ + Recent content in io_uring on JAUNT.SDN + Hugo -- gohugo.io + en-us + Sat, 02 Mar 2019 08:00:00 +0200 + + + + + + IO_URING for Netty based RPC: quick analysis + https://jauntsdn.com/post/rpc-iouring/ + Sat, 02 Mar 2019 08:00:00 +0200 + + https://jauntsdn.com/post/rpc-iouring/ + Netty IO_URING based transport was recently released as incubator project. Several sources have reported substantial performance gains with It, including Netty team TCP echo test results that demonstrated 2x better throughput compared to EPOLL. +The goal of this post is CPU and memory performance evaluation of IO_URING in RPC library implemented with Netty on top of TCP, and comparison against EPOLL IO. +It focuses on two use cases: + backend service: few dozens of connections, each serving ~1000 concurrent streams of few dozens messages each + + + + \ No newline at end of file diff --git a/tags/io_uring/page/1/index.html b/tags/io_uring/page/1/index.html new file mode 100644 index 0000000..0792df1 --- /dev/null +++ b/tags/io_uring/page/1/index.html @@ -0,0 +1 @@ +https://jauntsdn.com/tags/io_uring/ \ No newline at end of file diff --git a/tags/java/index.html b/tags/java/index.html new file mode 100644 index 0000000..3ad4c27 --- /dev/null +++ b/tags/java/index.html @@ -0,0 +1,271 @@ + + + + + + + + java + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    java

    + + + + +
    + + + + + + + + +

    RSocket-JVM: streamlining implementation for each vendor platform

    +
    April 22, 2021
    + +RSocket + +Mstreams + +java + + + +
    + + + +
    + + + +
    + + + + + + + +
    + + + +
    + +
    + + + + + + + + diff --git a/tags/java/index.xml b/tags/java/index.xml new file mode 100644 index 0000000..ed007af --- /dev/null +++ b/tags/java/index.xml @@ -0,0 +1,103 @@ + + + + java on JAUNT.SDN + https://jauntsdn.com/tags/java/ + Recent content in java on JAUNT.SDN + Hugo -- gohugo.io + en-us + Wed, 01 Feb 2023 10:18:00 +0200 + + + + + + 📌 Summary: alternative RSocket library for high performance network applications on JVM + https://jauntsdn.com/post/rsocket-summary/ + Wed, 01 Feb 2023 10:18:00 +0200 + + https://jauntsdn.com/post/rsocket-summary/ + Summary lean &amp; very fast GRPC-like services on JVM with rich streaming models over multiple data-center and internet transports. multiple APIs: CompletableFuture &amp; virtual threads; traditional streaming GRPC-stub (StreamObserver), or reactive with smallrye-mutiny, rxjava, reactor pluggable networking: tcp, unix sockets, VM sockets; grpc, websockets, websockets-over-http2 service APIs / codegen stubs (Message-Streams) are split from library runtime (RSocket-JVM, including network transports, load estimators, load balancers, metrics) transparent origin (RPC) &amp; proxy load estimation for low-cost yet efficient load balancers native image support with graalvm Project consists of API modules for each vendor library - Message-Streams, and respective runtime implementations - RSocket-JVM. + + + + Jaunt-RSocket-RPC, Spring-RSocket, GRPC: quantitative and qualitative comparison + https://jauntsdn.com/post/rsocket-vs-spring/ + Fri, 03 Sep 2021 10:18:00 +0200 + + https://jauntsdn.com/post/rsocket-vs-spring/ + Earlier this year we described weaknesses of Spring RSocket-java implementation that resulted in awful throughput, latency &amp; garbage production: key metrics for core networking library. +Latest Spring-RSocket uses RSocket/RSocket-java version 1.1.1, and wraps up almost a year of progress made on the library. +The goal of this post is to re-evaluate that progress against both current jauntsdn/RSocket-RPC (not yet publicly available), and grpc/grpc-java. +Jauntsdn-RSocket-RPC vs Spring-RSocket-CBOR: quantitative comparison jauntsdn/RSocket-RPC is remote procedure call system using streaming libraries (GRPC-API or several reactive - including RSocket-reactor) for networking &amp; APIs, and Protocol Buffers as sole data format. + + + + Alternative RSocket-RPC: fast application services communication and transparent GRPC bridge + https://jauntsdn.com/post/rsocket-grpc/ + Thu, 20 May 2021 08:00:00 +0200 + + https://jauntsdn.com/post/rsocket-grpc/ + Exchanging millions of buffers per second with plain RSockets is not useful for business applications because they need structured data instead of raw bytes. +That&rsquo;s why there was RSocket-RPC - remote procedure call system on top of Protocol Buffers with language agnostic service definitions. It uses code-generation for performance equivalent to hand-written code. +Protocol buffers have tiny on-wire overhead, acceptable performance and is native format of Grpc - important for interoperability and available tooling that can be used directly, or easily adapted. + + + + RSocket-JVM: streamlining implementation for each vendor platform + https://jauntsdn.com/post/rsocket-jvm/ + Thu, 22 Apr 2021 08:00:00 +0200 + + https://jauntsdn.com/post/rsocket-jvm/ + Motivation In practice reactive applications - as a method to model software and programming paradigm of composable flow controlled streams - viable mainly within JVM context. +It has necessary ecosystem provided by several competing platforms: smallrye-mutiny by Red Hat, project reactor by (ex-)Pivotal, and community-maintained rxjava. +Libraries are interoperable on same VM because their APIs semantically conform to reactive-streams specification, with 3 of 4 having binary compatibility - streams may be combined directly without writing any adapter code. + + + + Serving one million streams. Part 2. Double performance over original RSocket library + https://jauntsdn.com/post/rsocket-million-streams-2/ + Wed, 20 Jan 2021 10:00:00 +0200 + + https://jauntsdn.com/post/rsocket-million-streams-2/ + New year break is good time for continuing on topic of one million of streams - serving huge amount of RSocket streams simultaneously with single, mid-level commodity computer. +The original library gave in around 500k streams - mark where server was still stable, at whopping 500-600 MBytes/s allocation rate - unexpected numbers for RSockets just sending same byte buffer periodically, using library advertising zero-copy capability. +Interesting question is whether we hit natural limit, caused by current state of JVM runtime and libraries, or is a consequence of design choices driven at large by &ldquo;ideological&rdquo; and marketing motives that eventually resulted in implementation having significant parts happen to exist just for burning CPU cycles - both directly and through garbage collection? + + + + WebSockets over http/2: implementing RFC8441 with Netty + https://jauntsdn.com/post/netty-websocket-http2/ + Thu, 30 Jul 2020 11:00:00 +0200 + + https://jauntsdn.com/post/netty-websocket-http2/ + Today I&rsquo;d like to introduce you to netty-websocket-http2 - implementation of websockets-over-http2, first release of which is about to land on the Maven Central. +Novel protocol graduated out of draft phase in September 2018, and is one of the smallest RFCs I&rsquo;ve ever seen - just 4 pages of actual specification. +Despite tiny size It opens some interesting possibilities - for both clients and servers. +Protocol defines Extended CONNECT Method - mechanism for establishing byte stream tunnel over single http2 stream, and addresses biggest flaw of websocket over http1 - need for a separate tcp connection per websocket. + + + + Serving one million streams. Part 1. RSocket limits on the JVM + https://jauntsdn.com/post/rsocket-million-streams-1/ + Wed, 24 Jun 2020 19:30:00 +0200 + + https://jauntsdn.com/post/rsocket-million-streams-1/ + One of the interesting topics is estimation of RSocket limits in setup that approximates gateway serving tens of thousands clients. +There are numerous reports focusing on few streams over single connection, with emphasis on huge throughput. However this mode is not intrinsical for real server. Even though gateways are more likely to be cpu bound than IO bound, we can relax this assumption by making server logic trivial (just echo back incoming messages), to see how RSocket behaves under idealized conditions when major resource consumption is related to IO. + + + + Service concurrency limiting with RSocket request leases + https://jauntsdn.com/post/rsocket-lease-concurrency-limiting/ + Thu, 09 Apr 2020 17:00:00 +0200 + + https://jauntsdn.com/post/rsocket-lease-concurrency-limiting/ + This is first post in advanced applications of RSocket series, and topic for today is less known yet essential protocol feature - requests leasing. +We know that single RSocket stream is naturally bounded due to Reactive Streams semantics - there is no more in-flight messages than demanded with receiver Subscription. +However limiting outstanding messages per request is not useful without limiting requests concurrency. +For services, growing requests unbounded exhausts their backing resources and leads to latency surge. + + + + \ No newline at end of file diff --git a/tags/java/page/1/index.html b/tags/java/page/1/index.html new file mode 100644 index 0000000..8eb2a73 --- /dev/null +++ b/tags/java/page/1/index.html @@ -0,0 +1 @@ +https://jauntsdn.com/tags/java/ \ No newline at end of file diff --git a/tags/java/page/2/index.html b/tags/java/page/2/index.html new file mode 100644 index 0000000..89ba56b --- /dev/null +++ b/tags/java/page/2/index.html @@ -0,0 +1,231 @@ + + + + + + + + java + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    java

    + +
    + + + + + + + + +

    WebSockets over http/2: implementing RFC8441 with Netty

    +
    July 30, 2020
    + +netty + +websocket + +http2 + +java + + + +
    +
    + + + + + + + + +

    Serving one million streams. Part 1. RSocket limits on the JVM

    +
    June 24, 2020
    + +RSocket + +Mstreams + +java + + + +
    +
    + + + + + + + + +

    Service concurrency limiting with RSocket request leases

    +
    April 9, 2020
    + +RSocket + +Mstreams + +java + +load-balancer + + + +
    + + +
    + + + +
    + + + + + + + +
    + + + +
    + +
    + + + + + + + + diff --git a/tags/load-balancer/index.html b/tags/load-balancer/index.html new file mode 100644 index 0000000..cbf03d7 --- /dev/null +++ b/tags/load-balancer/index.html @@ -0,0 +1,173 @@ + + + + + + + + load-balancer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    load-balancer

    + +
    + + + + + + + + +

    Service concurrency limiting with RSocket request leases

    +
    April 9, 2020
    + +RSocket + +Mstreams + +java + +load-balancer + + + +
    + + +
    + + + +
    + +
    + + + + + + + + diff --git a/tags/load-balancer/index.xml b/tags/load-balancer/index.xml new file mode 100644 index 0000000..d492622 --- /dev/null +++ b/tags/load-balancer/index.xml @@ -0,0 +1,27 @@ + + + + load-balancer on JAUNT.SDN + https://jauntsdn.com/tags/load-balancer/ + Recent content in load-balancer on JAUNT.SDN + Hugo -- gohugo.io + en-us + Thu, 09 Apr 2020 17:00:00 +0200 + + + + + + Service concurrency limiting with RSocket request leases + https://jauntsdn.com/post/rsocket-lease-concurrency-limiting/ + Thu, 09 Apr 2020 17:00:00 +0200 + + https://jauntsdn.com/post/rsocket-lease-concurrency-limiting/ + This is first post in advanced applications of RSocket series, and topic for today is less known yet essential protocol feature - requests leasing. +We know that single RSocket stream is naturally bounded due to Reactive Streams semantics - there is no more in-flight messages than demanded with receiver Subscription. +However limiting outstanding messages per request is not useful without limiting requests concurrency. +For services, growing requests unbounded exhausts their backing resources and leads to latency surge. + + + + \ No newline at end of file diff --git a/tags/load-balancer/page/1/index.html b/tags/load-balancer/page/1/index.html new file mode 100644 index 0000000..a42f638 --- /dev/null +++ b/tags/load-balancer/page/1/index.html @@ -0,0 +1 @@ +https://jauntsdn.com/tags/load-balancer/ \ No newline at end of file diff --git a/tags/mstreams/index.html b/tags/mstreams/index.html new file mode 100644 index 0000000..3565fd6 --- /dev/null +++ b/tags/mstreams/index.html @@ -0,0 +1,271 @@ + + + + + + + + Mstreams + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    Mstreams

    + + + + +
    + + + + + + + + +

    RSocket-JVM: streamlining implementation for each vendor platform

    +
    April 22, 2021
    + +RSocket + +Mstreams + +java + + + +
    + + + +
    + + + +
    + + + + + + + +
    + + + +
    + +
    + + + + + + + + diff --git a/tags/mstreams/index.xml b/tags/mstreams/index.xml new file mode 100644 index 0000000..6f028bd --- /dev/null +++ b/tags/mstreams/index.xml @@ -0,0 +1,102 @@ + + + + Mstreams on JAUNT.SDN + https://jauntsdn.com/tags/mstreams/ + Recent content in Mstreams on JAUNT.SDN + Hugo -- gohugo.io + en-us + Wed, 01 Feb 2023 10:18:00 +0200 + + + + + + 📌 Summary: alternative RSocket library for high performance network applications on JVM + https://jauntsdn.com/post/rsocket-summary/ + Wed, 01 Feb 2023 10:18:00 +0200 + + https://jauntsdn.com/post/rsocket-summary/ + Summary lean &amp; very fast GRPC-like services on JVM with rich streaming models over multiple data-center and internet transports. multiple APIs: CompletableFuture &amp; virtual threads; traditional streaming GRPC-stub (StreamObserver), or reactive with smallrye-mutiny, rxjava, reactor pluggable networking: tcp, unix sockets, VM sockets; grpc, websockets, websockets-over-http2 service APIs / codegen stubs (Message-Streams) are split from library runtime (RSocket-JVM, including network transports, load estimators, load balancers, metrics) transparent origin (RPC) &amp; proxy load estimation for low-cost yet efficient load balancers native image support with graalvm Project consists of API modules for each vendor library - Message-Streams, and respective runtime implementations - RSocket-JVM. + + + + Jaunt-RSocket-RPC, Spring-RSocket, GRPC: quantitative and qualitative comparison + https://jauntsdn.com/post/rsocket-vs-spring/ + Fri, 03 Sep 2021 10:18:00 +0200 + + https://jauntsdn.com/post/rsocket-vs-spring/ + Earlier this year we described weaknesses of Spring RSocket-java implementation that resulted in awful throughput, latency &amp; garbage production: key metrics for core networking library. +Latest Spring-RSocket uses RSocket/RSocket-java version 1.1.1, and wraps up almost a year of progress made on the library. +The goal of this post is to re-evaluate that progress against both current jauntsdn/RSocket-RPC (not yet publicly available), and grpc/grpc-java. +Jauntsdn-RSocket-RPC vs Spring-RSocket-CBOR: quantitative comparison jauntsdn/RSocket-RPC is remote procedure call system using streaming libraries (GRPC-API or several reactive - including RSocket-reactor) for networking &amp; APIs, and Protocol Buffers as sole data format. + + + + Alternative RSocket-RPC: fast application services communication and transparent GRPC bridge + https://jauntsdn.com/post/rsocket-grpc/ + Thu, 20 May 2021 08:00:00 +0200 + + https://jauntsdn.com/post/rsocket-grpc/ + Exchanging millions of buffers per second with plain RSockets is not useful for business applications because they need structured data instead of raw bytes. +That&rsquo;s why there was RSocket-RPC - remote procedure call system on top of Protocol Buffers with language agnostic service definitions. It uses code-generation for performance equivalent to hand-written code. +Protocol buffers have tiny on-wire overhead, acceptable performance and is native format of Grpc - important for interoperability and available tooling that can be used directly, or easily adapted. + + + + RSocket-JVM: streamlining implementation for each vendor platform + https://jauntsdn.com/post/rsocket-jvm/ + Thu, 22 Apr 2021 08:00:00 +0200 + + https://jauntsdn.com/post/rsocket-jvm/ + Motivation In practice reactive applications - as a method to model software and programming paradigm of composable flow controlled streams - viable mainly within JVM context. +It has necessary ecosystem provided by several competing platforms: smallrye-mutiny by Red Hat, project reactor by (ex-)Pivotal, and community-maintained rxjava. +Libraries are interoperable on same VM because their APIs semantically conform to reactive-streams specification, with 3 of 4 having binary compatibility - streams may be combined directly without writing any adapter code. + + + + Serving one million streams. Part 2. Double performance over original RSocket library + https://jauntsdn.com/post/rsocket-million-streams-2/ + Wed, 20 Jan 2021 10:00:00 +0200 + + https://jauntsdn.com/post/rsocket-million-streams-2/ + New year break is good time for continuing on topic of one million of streams - serving huge amount of RSocket streams simultaneously with single, mid-level commodity computer. +The original library gave in around 500k streams - mark where server was still stable, at whopping 500-600 MBytes/s allocation rate - unexpected numbers for RSockets just sending same byte buffer periodically, using library advertising zero-copy capability. +Interesting question is whether we hit natural limit, caused by current state of JVM runtime and libraries, or is a consequence of design choices driven at large by &ldquo;ideological&rdquo; and marketing motives that eventually resulted in implementation having significant parts happen to exist just for burning CPU cycles - both directly and through garbage collection? + + + + Serving one million streams. Part 1. RSocket limits on the JVM + https://jauntsdn.com/post/rsocket-million-streams-1/ + Wed, 24 Jun 2020 19:30:00 +0200 + + https://jauntsdn.com/post/rsocket-million-streams-1/ + One of the interesting topics is estimation of RSocket limits in setup that approximates gateway serving tens of thousands clients. +There are numerous reports focusing on few streams over single connection, with emphasis on huge throughput. However this mode is not intrinsical for real server. Even though gateways are more likely to be cpu bound than IO bound, we can relax this assumption by making server logic trivial (just echo back incoming messages), to see how RSocket behaves under idealized conditions when major resource consumption is related to IO. + + + + Service concurrency limiting with RSocket request leases + https://jauntsdn.com/post/rsocket-lease-concurrency-limiting/ + Thu, 09 Apr 2020 17:00:00 +0200 + + https://jauntsdn.com/post/rsocket-lease-concurrency-limiting/ + This is first post in advanced applications of RSocket series, and topic for today is less known yet essential protocol feature - requests leasing. +We know that single RSocket stream is naturally bounded due to Reactive Streams semantics - there is no more in-flight messages than demanded with receiver Subscription. +However limiting outstanding messages per request is not useful without limiting requests concurrency. +For services, growing requests unbounded exhausts their backing resources and leads to latency surge. + + + + RSocket: short introduction + https://jauntsdn.com/post/rsocket-intro/ + Tue, 17 Mar 2020 11:30:00 +0200 + + https://jauntsdn.com/post/rsocket-intro/ + Description from official RSocket-java repository +RSocket is a binary protocol for use on byte stream transports... It enables the following symmetric interaction models via async message passing over a single connection.. leaves bits of mystery unless reader is interested enough to skim over lengthy FAQ and motivations documents. This post is one page size summary of distinctive RSocket features that make It a good companion for Http2/GRPC. +Low latency RSocket provides means for maintaining low latency under high throughput by limiting incoming and outgoing message queue size on both sides of connection: + + + + \ No newline at end of file diff --git a/tags/mstreams/page/1/index.html b/tags/mstreams/page/1/index.html new file mode 100644 index 0000000..de8b64a --- /dev/null +++ b/tags/mstreams/page/1/index.html @@ -0,0 +1 @@ +https://jauntsdn.com/tags/mstreams/ \ No newline at end of file diff --git a/tags/mstreams/page/2/index.html b/tags/mstreams/page/2/index.html new file mode 100644 index 0000000..0a1f601 --- /dev/null +++ b/tags/mstreams/page/2/index.html @@ -0,0 +1,227 @@ + + + + + + + + Mstreams + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    Mstreams

    + +
    + + + + + + + + +

    Serving one million streams. Part 1. RSocket limits on the JVM

    +
    June 24, 2020
    + +RSocket + +Mstreams + +java + + + +
    +
    + + + + + + + + +

    Service concurrency limiting with RSocket request leases

    +
    April 9, 2020
    + +RSocket + +Mstreams + +java + +load-balancer + + + +
    +
    + + + + + + + + +

    RSocket: short introduction

    +
    March 17, 2020
    + +RSocket + +Mstreams + + + +
    + + +
    + + + +
    + + + + + + + +
    + + + +
    + +
    + + + + + + + + diff --git a/tags/netty/index.html b/tags/netty/index.html new file mode 100644 index 0000000..ab9ec3e --- /dev/null +++ b/tags/netty/index.html @@ -0,0 +1,192 @@ + + + + + + + + netty + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    netty

    + +
    + + + + + + + + +

    WebSockets over http/2: implementing RFC8441 with Netty

    +
    July 30, 2020
    + +netty + +websocket + +http2 + +java + + + +
    +
    + + + + + + + + +

    IO_URING for Netty based RPC: quick analysis

    +
    March 2, 2019
    + +io_uring + +netty + + + +
    + + +
    + + + +
    + +
    + + + + + + + + diff --git a/tags/netty/index.xml b/tags/netty/index.xml new file mode 100644 index 0000000..9cdae91 --- /dev/null +++ b/tags/netty/index.xml @@ -0,0 +1,39 @@ + + + + netty on JAUNT.SDN + https://jauntsdn.com/tags/netty/ + Recent content in netty on JAUNT.SDN + Hugo -- gohugo.io + en-us + Thu, 30 Jul 2020 11:00:00 +0200 + + + + + + WebSockets over http/2: implementing RFC8441 with Netty + https://jauntsdn.com/post/netty-websocket-http2/ + Thu, 30 Jul 2020 11:00:00 +0200 + + https://jauntsdn.com/post/netty-websocket-http2/ + Today I&rsquo;d like to introduce you to netty-websocket-http2 - implementation of websockets-over-http2, first release of which is about to land on the Maven Central. +Novel protocol graduated out of draft phase in September 2018, and is one of the smallest RFCs I&rsquo;ve ever seen - just 4 pages of actual specification. +Despite tiny size It opens some interesting possibilities - for both clients and servers. +Protocol defines Extended CONNECT Method - mechanism for establishing byte stream tunnel over single http2 stream, and addresses biggest flaw of websocket over http1 - need for a separate tcp connection per websocket. + + + + IO_URING for Netty based RPC: quick analysis + https://jauntsdn.com/post/rpc-iouring/ + Sat, 02 Mar 2019 08:00:00 +0200 + + https://jauntsdn.com/post/rpc-iouring/ + Netty IO_URING based transport was recently released as incubator project. Several sources have reported substantial performance gains with It, including Netty team TCP echo test results that demonstrated 2x better throughput compared to EPOLL. +The goal of this post is CPU and memory performance evaluation of IO_URING in RPC library implemented with Netty on top of TCP, and comparison against EPOLL IO. +It focuses on two use cases: + backend service: few dozens of connections, each serving ~1000 concurrent streams of few dozens messages each + + + + \ No newline at end of file diff --git a/tags/netty/page/1/index.html b/tags/netty/page/1/index.html new file mode 100644 index 0000000..2588f21 --- /dev/null +++ b/tags/netty/page/1/index.html @@ -0,0 +1 @@ +https://jauntsdn.com/tags/netty/ \ No newline at end of file diff --git a/tags/page/1/index.html b/tags/page/1/index.html new file mode 100644 index 0000000..e3ca47f --- /dev/null +++ b/tags/page/1/index.html @@ -0,0 +1 @@ +https://jauntsdn.com/tags/ \ No newline at end of file diff --git a/tags/page/2/index.html b/tags/page/2/index.html new file mode 100644 index 0000000..46e838c --- /dev/null +++ b/tags/page/2/index.html @@ -0,0 +1,224 @@ + + + + + + + + Tags + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    Tags

    + +
    + + + + + + + + +

    netty

    +
    July 30, 2020
    + + + +
    +
    + + + + + + + + +

    websocket

    +
    July 30, 2020
    + + + +
    +
    + + + + + + + + +

    load-balancer

    +
    April 9, 2020
    + + + +
    +
    + + + + + + + + +

    io_uring

    +
    March 2, 2019
    + + + +
    + + +
    + + + +
    + + + + + + + +
    + + + +
    + +
    + + + + + + + + diff --git a/tags/rsocket/index.html b/tags/rsocket/index.html new file mode 100644 index 0000000..53d8a59 --- /dev/null +++ b/tags/rsocket/index.html @@ -0,0 +1,271 @@ + + + + + + + + RSocket + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    RSocket

    + + + + +
    + + + + + + + + +

    RSocket-JVM: streamlining implementation for each vendor platform

    +
    April 22, 2021
    + +RSocket + +Mstreams + +java + + + +
    + + + +
    + + + +
    + + + + + + + +
    + + + +
    + +
    + + + + + + + + diff --git a/tags/rsocket/index.xml b/tags/rsocket/index.xml new file mode 100644 index 0000000..b8f5c5b --- /dev/null +++ b/tags/rsocket/index.xml @@ -0,0 +1,102 @@ + + + + RSocket on JAUNT.SDN + https://jauntsdn.com/tags/rsocket/ + Recent content in RSocket on JAUNT.SDN + Hugo -- gohugo.io + en-us + Wed, 01 Feb 2023 10:18:00 +0200 + + + + + + 📌 Summary: alternative RSocket library for high performance network applications on JVM + https://jauntsdn.com/post/rsocket-summary/ + Wed, 01 Feb 2023 10:18:00 +0200 + + https://jauntsdn.com/post/rsocket-summary/ + Summary lean &amp; very fast GRPC-like services on JVM with rich streaming models over multiple data-center and internet transports. multiple APIs: CompletableFuture &amp; virtual threads; traditional streaming GRPC-stub (StreamObserver), or reactive with smallrye-mutiny, rxjava, reactor pluggable networking: tcp, unix sockets, VM sockets; grpc, websockets, websockets-over-http2 service APIs / codegen stubs (Message-Streams) are split from library runtime (RSocket-JVM, including network transports, load estimators, load balancers, metrics) transparent origin (RPC) &amp; proxy load estimation for low-cost yet efficient load balancers native image support with graalvm Project consists of API modules for each vendor library - Message-Streams, and respective runtime implementations - RSocket-JVM. + + + + Jaunt-RSocket-RPC, Spring-RSocket, GRPC: quantitative and qualitative comparison + https://jauntsdn.com/post/rsocket-vs-spring/ + Fri, 03 Sep 2021 10:18:00 +0200 + + https://jauntsdn.com/post/rsocket-vs-spring/ + Earlier this year we described weaknesses of Spring RSocket-java implementation that resulted in awful throughput, latency &amp; garbage production: key metrics for core networking library. +Latest Spring-RSocket uses RSocket/RSocket-java version 1.1.1, and wraps up almost a year of progress made on the library. +The goal of this post is to re-evaluate that progress against both current jauntsdn/RSocket-RPC (not yet publicly available), and grpc/grpc-java. +Jauntsdn-RSocket-RPC vs Spring-RSocket-CBOR: quantitative comparison jauntsdn/RSocket-RPC is remote procedure call system using streaming libraries (GRPC-API or several reactive - including RSocket-reactor) for networking &amp; APIs, and Protocol Buffers as sole data format. + + + + Alternative RSocket-RPC: fast application services communication and transparent GRPC bridge + https://jauntsdn.com/post/rsocket-grpc/ + Thu, 20 May 2021 08:00:00 +0200 + + https://jauntsdn.com/post/rsocket-grpc/ + Exchanging millions of buffers per second with plain RSockets is not useful for business applications because they need structured data instead of raw bytes. +That&rsquo;s why there was RSocket-RPC - remote procedure call system on top of Protocol Buffers with language agnostic service definitions. It uses code-generation for performance equivalent to hand-written code. +Protocol buffers have tiny on-wire overhead, acceptable performance and is native format of Grpc - important for interoperability and available tooling that can be used directly, or easily adapted. + + + + RSocket-JVM: streamlining implementation for each vendor platform + https://jauntsdn.com/post/rsocket-jvm/ + Thu, 22 Apr 2021 08:00:00 +0200 + + https://jauntsdn.com/post/rsocket-jvm/ + Motivation In practice reactive applications - as a method to model software and programming paradigm of composable flow controlled streams - viable mainly within JVM context. +It has necessary ecosystem provided by several competing platforms: smallrye-mutiny by Red Hat, project reactor by (ex-)Pivotal, and community-maintained rxjava. +Libraries are interoperable on same VM because their APIs semantically conform to reactive-streams specification, with 3 of 4 having binary compatibility - streams may be combined directly without writing any adapter code. + + + + Serving one million streams. Part 2. Double performance over original RSocket library + https://jauntsdn.com/post/rsocket-million-streams-2/ + Wed, 20 Jan 2021 10:00:00 +0200 + + https://jauntsdn.com/post/rsocket-million-streams-2/ + New year break is good time for continuing on topic of one million of streams - serving huge amount of RSocket streams simultaneously with single, mid-level commodity computer. +The original library gave in around 500k streams - mark where server was still stable, at whopping 500-600 MBytes/s allocation rate - unexpected numbers for RSockets just sending same byte buffer periodically, using library advertising zero-copy capability. +Interesting question is whether we hit natural limit, caused by current state of JVM runtime and libraries, or is a consequence of design choices driven at large by &ldquo;ideological&rdquo; and marketing motives that eventually resulted in implementation having significant parts happen to exist just for burning CPU cycles - both directly and through garbage collection? + + + + Serving one million streams. Part 1. RSocket limits on the JVM + https://jauntsdn.com/post/rsocket-million-streams-1/ + Wed, 24 Jun 2020 19:30:00 +0200 + + https://jauntsdn.com/post/rsocket-million-streams-1/ + One of the interesting topics is estimation of RSocket limits in setup that approximates gateway serving tens of thousands clients. +There are numerous reports focusing on few streams over single connection, with emphasis on huge throughput. However this mode is not intrinsical for real server. Even though gateways are more likely to be cpu bound than IO bound, we can relax this assumption by making server logic trivial (just echo back incoming messages), to see how RSocket behaves under idealized conditions when major resource consumption is related to IO. + + + + Service concurrency limiting with RSocket request leases + https://jauntsdn.com/post/rsocket-lease-concurrency-limiting/ + Thu, 09 Apr 2020 17:00:00 +0200 + + https://jauntsdn.com/post/rsocket-lease-concurrency-limiting/ + This is first post in advanced applications of RSocket series, and topic for today is less known yet essential protocol feature - requests leasing. +We know that single RSocket stream is naturally bounded due to Reactive Streams semantics - there is no more in-flight messages than demanded with receiver Subscription. +However limiting outstanding messages per request is not useful without limiting requests concurrency. +For services, growing requests unbounded exhausts their backing resources and leads to latency surge. + + + + RSocket: short introduction + https://jauntsdn.com/post/rsocket-intro/ + Tue, 17 Mar 2020 11:30:00 +0200 + + https://jauntsdn.com/post/rsocket-intro/ + Description from official RSocket-java repository +RSocket is a binary protocol for use on byte stream transports... It enables the following symmetric interaction models via async message passing over a single connection.. leaves bits of mystery unless reader is interested enough to skim over lengthy FAQ and motivations documents. This post is one page size summary of distinctive RSocket features that make It a good companion for Http2/GRPC. +Low latency RSocket provides means for maintaining low latency under high throughput by limiting incoming and outgoing message queue size on both sides of connection: + + + + \ No newline at end of file diff --git a/tags/rsocket/page/1/index.html b/tags/rsocket/page/1/index.html new file mode 100644 index 0000000..004f37d --- /dev/null +++ b/tags/rsocket/page/1/index.html @@ -0,0 +1 @@ +https://jauntsdn.com/tags/rsocket/ \ No newline at end of file diff --git a/tags/rsocket/page/2/index.html b/tags/rsocket/page/2/index.html new file mode 100644 index 0000000..0504866 --- /dev/null +++ b/tags/rsocket/page/2/index.html @@ -0,0 +1,227 @@ + + + + + + + + RSocket + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    RSocket

    + +
    + + + + + + + + +

    Serving one million streams. Part 1. RSocket limits on the JVM

    +
    June 24, 2020
    + +RSocket + +Mstreams + +java + + + +
    +
    + + + + + + + + +

    Service concurrency limiting with RSocket request leases

    +
    April 9, 2020
    + +RSocket + +Mstreams + +java + +load-balancer + + + +
    +
    + + + + + + + + +

    RSocket: short introduction

    +
    March 17, 2020
    + +RSocket + +Mstreams + + + +
    + + +
    + + + +
    + + + + + + + +
    + + + +
    + +
    + + + + + + + + diff --git a/tags/websocket/index.html b/tags/websocket/index.html new file mode 100644 index 0000000..426ae11 --- /dev/null +++ b/tags/websocket/index.html @@ -0,0 +1,173 @@ + + + + + + + + websocket + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    websocket

    + +
    + + + + + + + + +

    WebSockets over http/2: implementing RFC8441 with Netty

    +
    July 30, 2020
    + +netty + +websocket + +http2 + +java + + + +
    + + +
    + + + +
    + +
    + + + + + + + + diff --git a/tags/websocket/index.xml b/tags/websocket/index.xml new file mode 100644 index 0000000..440fe3f --- /dev/null +++ b/tags/websocket/index.xml @@ -0,0 +1,27 @@ + + + + websocket on JAUNT.SDN + https://jauntsdn.com/tags/websocket/ + Recent content in websocket on JAUNT.SDN + Hugo -- gohugo.io + en-us + Thu, 30 Jul 2020 11:00:00 +0200 + + + + + + WebSockets over http/2: implementing RFC8441 with Netty + https://jauntsdn.com/post/netty-websocket-http2/ + Thu, 30 Jul 2020 11:00:00 +0200 + + https://jauntsdn.com/post/netty-websocket-http2/ + Today I&rsquo;d like to introduce you to netty-websocket-http2 - implementation of websockets-over-http2, first release of which is about to land on the Maven Central. +Novel protocol graduated out of draft phase in September 2018, and is one of the smallest RFCs I&rsquo;ve ever seen - just 4 pages of actual specification. +Despite tiny size It opens some interesting possibilities - for both clients and servers. +Protocol defines Extended CONNECT Method - mechanism for establishing byte stream tunnel over single http2 stream, and addresses biggest flaw of websocket over http1 - need for a separate tcp connection per websocket. + + + + \ No newline at end of file diff --git a/tags/websocket/page/1/index.html b/tags/websocket/page/1/index.html new file mode 100644 index 0000000..a2ef25d --- /dev/null +++ b/tags/websocket/page/1/index.html @@ -0,0 +1 @@ +https://jauntsdn.com/tags/websocket/ \ No newline at end of file