diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b407db39c0a..1b446cc03ac 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,23 @@ +#### 1.4.35 March 18 2022 #### +Akka.NET v1.4.35 is a minor release that contains some bug fixes. Most of the changes have been aimed at improving our web documentation and code cleanup to modernize some of our code. + +__Bug fixes__: +* [Akka: Fixed IActorRef leak inside EventStream](https://github.com/akkadotnet/akka.net/pull/5720) +* [Akka: Fixed ActorSystemSetup.And forgetting registered types](https://github.com/akkadotnet/akka.net/issues/5728) +* [Akka.Persistence.Query.Sql: Fixed Query PersistenceIds query bug](https://github.com/akkadotnet/akka.net/pull/5715) +* [Akka.Streams: Add MapMaterializedValue for SourceWithContext and FlowWithContext](https://github.com/akkadotnet/akka.net/pull/5711) + +If you want to see the [full set of changes made in Akka.NET v1.4.35, click here](https://github.com/akkadotnet/akka.net/milestone/66?closed=1). + +| COMMITS | LOC+ | LOC- | AUTHOR | +|---------|------|------|---------------------| +| 6 | 2178 | 174 | Aaron Stannard | +| 2 | 43 | 33 | Gregorius Soedharmo | +| 1 | 71 | 19 | Ismael Hamed | +| 1 | 1 | 1 | dependabot[bot] | + #### 1.4.34 March 7 2022 #### -Akka.NET v1.4.33 is a minor release that contains some bug fixes. Most of the changes have been aimed at improving our web documentation and code cleanup to modernize some of our code. +Akka.NET v1.4.34 is a minor release that contains some bug fixes. Most of the changes have been aimed at improving our web documentation and code cleanup to modernize some of our code. __Bug fixes__: * [Akka: Added support to pass a state object into CircuitBreaker to reduce allocations](https://github.com/akkadotnet/akka.net/pull/5650) @@ -9,7 +27,7 @@ __Bug fixes__: __Improvements__: * [Akka: PipeTo can be configured to retain async threading context](https://github.com/akkadotnet/akka.net/pull/5684) -If you want to see the [full set of changes made in Akka.NET v1.4.33, click here](https://github.com/akkadotnet/akka.net/milestone/65?closed=1). +If you want to see the [full set of changes made in Akka.NET v1.4.34, click here](https://github.com/akkadotnet/akka.net/milestone/65?closed=1). | COMMITS | LOC+ | LOC- | AUTHOR | |---------|--------|-------|---------------------| diff --git a/docs/articles/clustering/cluster-overview.md b/docs/articles/clustering/cluster-overview.md index 4dd1c553115..a022b6a1ecd 100644 --- a/docs/articles/clustering/cluster-overview.md +++ b/docs/articles/clustering/cluster-overview.md @@ -11,7 +11,7 @@ A cluster represents a fault-tolerant, elastic, decentralized peer-to-peer netwo ## What Does Akka.Cluster Do? -The best way to begin introducing Akka.Cluster is with brief overview of what it does. Akka.Cluster is the [package](https://www.nuget.org/packages/Akka.Cluster/1.0.3.11-beta) that brings clustering support to Akka.NET, and it accomplishes this by adding the following capabilities to Akka.NET: +The best way to begin introducing Akka.Cluster is with brief overview of what it does. Akka.Cluster is the [package](https://www.nuget.org/packages/Akka.Cluster/) that brings clustering support to Akka.NET, and it accomplishes this by adding the following capabilities to Akka.NET: * Makes it easy to create peer-to-peer networks of Akka.NET applications * Allows peers to automatically discover new nodes and removed dead ones automatically with no configuration changes diff --git a/docs/community/community-standups.md b/docs/community/community-standups.md new file mode 100644 index 00000000000..14b64666169 --- /dev/null +++ b/docs/community/community-standups.md @@ -0,0 +1,68 @@ +--- +uid: community-standups +title: Akka.NET Community Stand-Ups +--- + +# Akka.NET Community Stand-Ups + +Akka.NET Community Stand-ups are a once-a-month meeting that happens on the second Wednesday of each month and is live-streamed via [Petabridge’s YouTube channel](https://www.youtube.com/c/PetabridgeAcademy). + +> [!TIP] +> [Click here to subscribe for future Akka.NET Community Stand-up notifications on YouTube](https://www.youtube.com/c/PetabridgeAcademy?sub_confirmation=1) + +## Structure of Each Stand-Up + +1. **Recap of most recent Akka.NET releases (10 min)** - we should talk about the work that was performed since the last stand-up, its impact on end-users, and what we learned in the course of performing that work. +2. **Plans for future work (10 min)** - what are we working on next, why, and why should users care? +3. **Opportunities to contribute (5 min)** - what help would we like from the Akka.NET end-user community? +4. **Show and Tell (20 min)** - give members of the Akka.NET team and community opportunities to showcase their work built on top of Akka.NET. +5. **Q&A (10 min)** - show and tell presenters along with the Akka.NET team should take questions via YouTube Live from the audience. + +### Current Schedule + +**Stand-ups are second Wednesday of each month starting at 12pm CST.** + +> [!TIP] +> See our [schedule of upcoming 2022 Akka.NET Community Stand-ups here](https://github.com/akkadotnet/akka.net/discussions/5691). + +## Show and Tell Presentations + +The AKka.NET team features members of the Akka.NET community at each stand-up, where they their stories about how they use Akka.NET at work. e call these "Show and Tell" presentations and they are featured in our Community Stand-ups going forward. + +> [!IMPORTANT] +> Show and Tell is a great way to attract talented .NET developers in the Akka.NET community who love to work on the sorts of interesting and important problems that Akka.NET solves well. Use this as a platform to showcase your company and attract great talent to it! + +You can participate in the Show and Tell process by filling out this form: [Akka.NET Community Stand-up Submission](https://forms.gle/zR1K2pygCFhdCG7v5) - someone from our team will respond, help you with your presentation, and get you scheduled for one of our upcoming 2022 Akka.NET Community Stand-ups. + +### Show and Tell Structure + +Show and tell is an important component of each set of community stand-ups as it gives members of the Akka.NET Community a chance to share their experiences and success with Akka.NET. + +Here is what makes for a compelling show and tell presentation: + +1. Presenter Background - who are you, who do you work for, and what do you do? +2. Project Background - why is this project important to you and your company? +3. Requirements - what requirements did this project have? +4. Why Akka.NET - why did you choose Akka.NET? What was the criteria? What alternatives did you consider? +5. Which parts of Akka.NET do you use and why? +6. Was this a greenfield project or an update to a legacy system? What were the key implementation challenges with Akka.NET? +7. How has the system been performing for you since? +8. Visualization - do you have some diagrams that can help show how the system runs? +9. Any future plans for Akka.NET on your project or at your company? + +### Production + +After you've submitted the form, a member of the Akka.NET team will contact you and help you put together a PowerPoint presentation or a live demo that helps "show" all of the answers to your questions above. This is a great opportunity for you and your company to showcase the interesting work that you do and attract potential Akka.NET talent to your company. + +### Past Show and Tells + +Examples of past show and tells: + +* [Game Plumbing Framework on Akka.NET](https://youtu.be/6AoDlK8LtFU?t=958) - July 7th, 2021 + +### Submit a “Show and Tell” + +Want to share your experience with Akka.NET? Fill out our Google Form here and someone from our team will contact you about participating in an upcoming Akka.NET Community Stand-up! + +> [!TIP] +> Click here to submit a "Show and Tell" proposal to the Akka.NET team: [Akka.NET Community Stand-up Submission](https://forms.gle/zR1K2pygCFhdCG7v5) diff --git a/docs/community/toc.yml b/docs/community/toc.yml index 15670fc9d60..afbc0aba385 100644 --- a/docs/community/toc.yml +++ b/docs/community/toc.yml @@ -2,6 +2,8 @@ href: whats-new/toc.yml - name: Contributing to Akka.NET href: contributing/toc.yml +- name: Community Standups + href: community-standups.md - name: Getting Access to Nightly Akka.NET Builds href: getting-access-to-nightly-builds.md - name: Online Resources diff --git a/docs/community/whats-new/akkadotnet-v1.5.md b/docs/community/whats-new/akkadotnet-v1.5.md new file mode 100644 index 00000000000..5912014ccc1 --- /dev/null +++ b/docs/community/whats-new/akkadotnet-v1.5.md @@ -0,0 +1,131 @@ +--- +uid: akkadotnet-v15-whats-new +title: What's new in Akka.NET v1.5.0? +--- + +# Akka.NET v1.5 Plans and Goals + +Beginning with our [March 9th, 2022 Akka.NET Community Standup](xref:community-standups) we've shared our plans for the roadmap of Akka.NET v1.5. + +While not every technical detail is finalized yet, we do want to share the goals and some of the specific plans of what's included in this next minor version release of Akka.NET! + +## Goals + + + + + +Based on feedback from Akka.NET users, here are the top priorities: + +1. **Better documentation and examples** - after simplifying the Akka.NET APIs around configuration and startup, modernizing all samples and deployment guidance is the biggest issue affecting end-users. +2. **Improved Akka.Remote + Cluster.Sharding performance** - we want to go from ~350,000 msg/s to 1m+ msg/s in our `RemotePingPong` benchmark by changing the design of Akka.Remote's actors and optionally, replacing our transports. +3. **Improved default serialization options** - Newtonsoft.Json sucks. Full stop. +4. **Improved Akka.Cluster formation experience** - in layman's terms this means [releasing Akka.Management out of beta](https://github.com/akkadotnet/Akka.Management/discussions/417) and incorporating it into the Akka.Cluster literature and code samples. +5. **Leveraging .NET 6+ primitives for improved performance** - thread execution, zero-allocation System.Memory data structures, and more. +6. **Make CQRS a priority in Akka.Persistence** - tags are an especially weak area for Akka.Persistence right now, given that they require table scans. +7. **Leverage Microsoft.Extensions more closely** - consume Microsoft.Extensions.Configuration from HOCON, allow Microsoft.Extensions.Logging interplay, and more. +8. **Cleanup** - there are a lot of old constructs and that can be removed from Akka.NET. + +## How to Contribute + +Contributing to the v1.5 effort is somewhat different than our [normal maintenance contributions to Akka.NET](xref:contributing-to-akkadotnet),in the following ways: + +1. **Breaking binary compatibility changes are allowed so long as they are cost-justified** - contributors must be able to explain both the benefits and trade-offs involved in making this change in order for it to be accepted; +2. **Wire compatibility must include a viable upgrade path** - if we're to introduce changes to the wire format of Akka.NET, contributors must demonstrate and document a viable upgrade path for end-users; and +3. **Bigger bets are encouraged** - now is a good time to take some risks on the code base. So long as contributors can offer sound arguments for taking those bets (and this includes acknowledging and being realistic about trade-offs) then they should feel free to propose new changes that align with the 1.5 goals. + +### Good Areas for Contribution + +1. .NET 6 dual-targeting - there will be lots of areas where we can take advantage of new .NET 6 APIs for improved performance; +2. Performance optimization - performance optimization around Akka.Remote, Akka.Persistence, Akka.Cluster.Sharding, DData, and core Akka will be prioritized in this release and there are _many_ areas for improvement; +3. Akka.Hosting - there are lots of extension points and possibilities for making the APIs as expressive + concise as possible - we will need input from contributors to help make this possible; +4. API Cleanup - there are lots of utility classes or deprecated APIs that can be deleted from the code base with minimal impact. Less is more; and +5. New ideas and possibilities - this is a _great_ time to consider adding new features to Akka.NET. Be bold. + +> [!IMPORTANT] +> Familiarize yourself with our [Akka.NET contribution guidelines](xref:contributing-to-akkadotnet) for best experience. + +## Designs & Major Changes + +### Akka.Hosting + +We want to make Akka.NET something that can be instantiated more typically per the patterns often used with the Microsoft.Extensions.Hosting APIs that are common throughout .NET. + +```csharp +using Akka.Hosting; +using Akka.Actor; +using Akka.Actor.Dsl; +using Akka.Cluster.Hosting; +using Akka.Remote.Hosting; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAkka("MyActorSystem", configurationBuilder => +{ + configurationBuilder + .WithRemoting("localhost", 8110) + .WithClustering(new ClusterOptions(){ Roles = new[]{ "myRole" }, + SeedNodes = new[]{ Address.Parse("akka.tcp://MyActorSystem@localhost:8110")}}) + .WithActors((system, registry) => + { + var echo = system.ActorOf(act => + { + act.ReceiveAny((o, context) => + { + context.Sender.Tell($"{context.Self} rcv {o}"); + }); + }, "echo"); + registry.TryRegister(echo); // register for DI + }); +}); + +var app = builder.Build(); + +app.MapGet("/", async (context) => +{ + var echo = context.RequestServices.GetRequiredService().Get(); + var body = await echo.Ask(context.TraceIdentifier, context.RequestAborted).ConfigureAwait(false); + await context.Response.WriteAsync(body); +}); + +app.Run(); +``` + +No HOCON. Automatically runs all Akka.NET application lifecycle best practices behind the scene. Automatically binds the `ActorSystem` and the `ActorRegistry`, another new 1.5 feature, to the `IServiceCollection` so they can be safely consumed via both actors and non-Akka.NET parts of users' .NET applications. + +This should be open to extension in other child plugins, such as Akka.Persistence.SqlServer: + +```csharp +builder.Services.AddAkka("MyActorSystem", configurationBuilder => +{ + configurationBuilder + .WithRemoting("localhost", 8110) + .WithClustering(new ClusterOptions() + { + Roles = new[] { "myRole" }, + SeedNodes = new[] { Address.Parse("akka.tcp://MyActorSystem@localhost:8110") } + }) + .WithSqlServerPersistence(builder.Configuration.GetConnectionString("sqlServerLocal")) + .WithShardRegion("userActions", s => UserActionsEntity.Props(s), + new UserMessageExtractor(), + new ShardOptions(){ StateStoreMode = StateStoreMode.DData, Role = "myRole"}) + .WithActors((system, registry) => + { + var userActionsShard = registry.Get(); + var indexer = system.ActorOf(Props.Create(() => new Indexer(userActionsShard)), "index"); + registry.TryRegister(indexer); // register for DI + }); +}) +``` + +#### `ActorRegistry` + +As part of Akka.Hosting, we need to provide a means of making it easy to pass around top-level `IActorRef`s via dependency injection both within the `ActorSystem` and outside of it. + +The `ActorRegistry` will fulfill this role through a set of generic, typed methods that make storage and retrieval of long-lived `IActorRef`s easy and coherent: + +```csharp +var registry = ActorRegistry.For(myActorSystem); // fetch from ActorSystem +registry.TryRegister(indexer); // register for DI +registry.Get(); // use in DI +``` diff --git a/docs/community/whats-new/toc.yml b/docs/community/whats-new/toc.yml index 05b7ff9aaaa..3aeb7c05559 100644 --- a/docs/community/whats-new/toc.yml +++ b/docs/community/whats-new/toc.yml @@ -1,3 +1,5 @@ +- name: New in Akka.NET v1.5 + href: akkadotnet-v1.5.md - name: New in Akka.NET v1.4 href: akkadotnet-v1.4.md - name: Upgrade Advisories (v1.4) diff --git a/docs/docfx.json b/docs/docfx.json index 88cb66fd359..ae77b338564 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -67,7 +67,6 @@ "_appTitle": "Akka.NET Documentation", "_appLogoPath": "/images/akkalogo.png", "_appFaviconPath": "/images/favicon.ico", - "_appFooter": "Copyright © 2013-2021 Akka.NET project
Generated by DocFX", "_enableSearch": "true" }, "dest": "_site", diff --git a/docs/images/footer.png b/docs/images/footer.png new file mode 100644 index 00000000000..f615ad5a558 Binary files /dev/null and b/docs/images/footer.png differ diff --git a/docs/images/github.png b/docs/images/github.png new file mode 100644 index 00000000000..628da97c708 Binary files /dev/null and b/docs/images/github.png differ diff --git a/docs/images/header.svg b/docs/images/header.svg new file mode 100644 index 00000000000..8159657f241 --- /dev/null +++ b/docs/images/header.svg @@ -0,0 +1,31 @@ + + + Created with Lunacy + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/images/logos/petabridge_logo_full.png b/docs/images/logos/petabridge_logo_full.png new file mode 100644 index 00000000000..df920021237 Binary files /dev/null and b/docs/images/logos/petabridge_logo_full.png differ diff --git a/docs/images/logos/petabridge_square.png b/docs/images/logos/petabridge_square.png new file mode 100644 index 00000000000..c68f732dfdf Binary files /dev/null and b/docs/images/logos/petabridge_square.png differ diff --git a/docs/index.md b/docs/index.md index 9c669f1ed00..69cfc346037 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,6 +4,7 @@ title: Akka.NET Documentation tagline: A straightforward approach to building distributed, high-scale applications in .NET --- + -
-
+
+

Try Akka.NET now!

Follow our tutorial and build your first Akka.NET application today.

- What's new in Akka.NET v1.4.0? + Coming Soon: Akka.NET v1.5 Get Started Now Read the documentation
+
+
+

Professionally Maintained and Supported by Petabridge

+ Petabridge, the Akka.NET Company. +
+
+

Build powerful concurrent & distributed applications more easily.

diff --git a/docs/template/index.html.tmpl b/docs/template/index.html.tmpl index 4480cd88f32..a4dc9dba918 100644 --- a/docs/template/index.html.tmpl +++ b/docs/template/index.html.tmpl @@ -10,7 +10,7 @@
{{>partials/navbar}}
-
+
{{{conceptual}}}
{{>partials/footer}} diff --git a/docs/template/layout/_master.tmpl b/docs/template/layout/_master.tmpl new file mode 100644 index 00000000000..0113db54f46 --- /dev/null +++ b/docs/template/layout/_master.tmpl @@ -0,0 +1,55 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} +{{!include(/^styles/.*/)}} +{{!include(/^fonts/.*/)}} +{{!include(favicon.ico)}} +{{!include(logo.svg)}} +{{!include(search-stopwords.json)}} + + + + {{>partials/head}} + +
+
+ {{^_disableNavbar}} + {{>partials/navbar}} + {{/_disableNavbar}} + {{^_disableBreadcrumb}} + {{>partials/breadcrumb}} + {{/_disableBreadcrumb}} +
+ {{#_enableSearch}} +
+ {{>partials/searchResults}} +
+ {{/_enableSearch}} +
+ {{^_disableToc}} + {{>partials/toc}} +
+ {{/_disableToc}} + {{#_disableToc}} +
+ {{/_disableToc}} + {{#_disableAffix}} +
+ {{/_disableAffix}} + {{^_disableAffix}} +
+ {{/_disableAffix}} +
+ {{!body}} +
+
+ {{^_disableAffix}} + {{>partials/affix}} + {{/_disableAffix}} +
+
+ {{^_disableFooter}} + {{>partials/footer}} + {{/_disableFooter}} +
+ {{>partials/scripts}} + + diff --git a/docs/template/partials/affix.tmpl.partial b/docs/template/partials/affix.tmpl.partial new file mode 100644 index 00000000000..3a9a1ea9c7f --- /dev/null +++ b/docs/template/partials/affix.tmpl.partial @@ -0,0 +1,26 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} + +
+
+
+
{{__global.inThisArticle}}
+
+
+ {{^_disableContribution}} +
+
    + {{#docurl}} +
  • + github{{__global.improveThisDoc}} +
  • + {{/docurl}} + {{#sourceurl}} +
  • + {{__global.viewSource}} +
  • + {{/sourceurl}} +
+
+ {{/_disableContribution}} +
+
diff --git a/docs/template/partials/footer.tmpl.partial b/docs/template/partials/footer.tmpl.partial new file mode 100644 index 00000000000..e53b01992fe --- /dev/null +++ b/docs/template/partials/footer.tmpl.partial @@ -0,0 +1,36 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} + +
+
+
+
+ + {{__global.backToTop}} + + {{{_appFooter}}} + {{^_appFooter}} +
+ Contribute +
    +
  • Project Chat
  • +
  • Discussion Forum
  • +
  • Source Code
  • +
+
+
Support +
    +
  • Akka.NET Support Plans
  • +
  • Akka.NET Observability Tools
  • +
  • Akka.NET Training & Consulting
  • +
+
+
Maintained By +
    +
  • Petabridge - The Akka.NET Company
  • +
  • Learn Akka.NET
  • +
+
+ {{/_appFooter}} +
+
+
diff --git a/docs/template/partials/navbar.tmpl.partial b/docs/template/partials/navbar.tmpl.partial index e77ce32dcaa..8b75e3f0cbd 100644 --- a/docs/template/partials/navbar.tmpl.partial +++ b/docs/template/partials/navbar.tmpl.partial @@ -4,7 +4,7 @@ img.akka-logo{ }
-
+
- {{_appName}} + {{_appName}}
diff --git a/docs/template/styles/docfx.js b/docs/template/styles/docfx.js new file mode 100644 index 00000000000..8568b6d940b --- /dev/null +++ b/docs/template/styles/docfx.js @@ -0,0 +1,1241 @@ +// Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information. +$(function () { + var active = 'active'; + var expanded = 'in'; + var collapsed = 'collapsed'; + var filtered = 'filtered'; + var show = 'show'; + var hide = 'hide'; + var util = new utility(); + + workAroundFixedHeaderForAnchors(); + highlight(); + enableSearch(); + + renderTables(); + renderAlerts(); + renderLinks(); + renderNavbar(); + renderSidebar(); + renderAffix(); + renderFooter(); + renderLogo(); + + breakText(); + renderTabs(); + + window.refresh = function (article) { + // Update markup result + if (typeof article == 'undefined' || typeof article.content == 'undefined') + console.error("Null Argument"); + $("article.content").html(article.content); + + highlight(); + renderTables(); + renderAlerts(); + renderAffix(); + renderTabs(); + } + + // Add this event listener when needed + // window.addEventListener('content-update', contentUpdate); + + function breakText() { + $(".xref").addClass("text-break"); + var texts = $(".text-break"); + texts.each(function () { + $(this).breakWord(); + }); + } + + // Styling for tables in conceptual documents using Bootstrap. + // See http://getbootstrap.com/css/#tables + function renderTables() { + $('table').addClass('table table-bordered table-striped table-condensed').wrap('
'); + } + + // Styling for alerts. + function renderAlerts() { + $('.NOTE, .TIP').addClass('alert alert-info'); + $('.WARNING').addClass('alert alert-warning'); + $('.IMPORTANT, .CAUTION').addClass('alert alert-danger'); + } + + // Enable anchors for headings. + (function () { + anchors.options = { + placement: 'left', + visible: 'hover' + }; + anchors.add('article h2:not(.no-anchor), article h3:not(.no-anchor), article h4:not(.no-anchor)'); + })(); + + // Open links to different host in a new window. + function renderLinks() { + if ($("meta[property='docfx:newtab']").attr("content") === "true") { + $(document.links).filter(function () { + return this.hostname !== window.location.hostname; + }).attr('target', '_blank'); + } + } + + // Enable highlight.js + function highlight() { + $('pre code').each(function (i, block) { + hljs.highlightBlock(block); + }); + $('pre code[highlight-lines]').each(function (i, block) { + if (block.innerHTML === "") return; + var lines = block.innerHTML.split('\n'); + + queryString = block.getAttribute('highlight-lines'); + if (!queryString) return; + + var ranges = queryString.split(','); + for (var j = 0, range; range = ranges[j++];) { + var found = range.match(/^(\d+)\-(\d+)?$/); + if (found) { + // consider region as `{startlinenumber}-{endlinenumber}`, in which {endlinenumber} is optional + var start = +found[1]; + var end = +found[2]; + if (isNaN(end) || end > lines.length) { + end = lines.length; + } + } else { + // consider region as a sigine line number + if (isNaN(range)) continue; + var start = +range; + var end = start; + } + if (start <= 0 || end <= 0 || start > end || start > lines.length) { + // skip current region if invalid + continue; + } + lines[start - 1] = '' + lines[start - 1]; + lines[end - 1] = lines[end - 1] + ''; + } + + block.innerHTML = lines.join('\n'); + }); + } + + // Support full-text-search + function enableSearch() { + var query; + var relHref = $("meta[property='docfx\\:rel']").attr("content"); + if (typeof relHref === 'undefined') { + return; + } + try { + var worker = new Worker(relHref + 'styles/search-worker.js'); + if (!worker && !window.worker) { + localSearch(); + } else { + webWorkerSearch(); + } + + renderSearchBox(); + highlightKeywords(); + addSearchEvent(); + } catch (e) { + console.error(e); + } + + //Adjust the position of search box in navbar + function renderSearchBox() { + autoCollapse(); + $(window).on('resize', autoCollapse); + $(document).on('click', '.navbar-collapse.in', function (e) { + if ($(e.target).is('a')) { + $(this).collapse('hide'); + } + }); + + function autoCollapse() { + var navbar = $('#autocollapse'); + if (navbar.height() === null) { + setTimeout(autoCollapse, 300); + } + navbar.removeClass(collapsed); + if (navbar.height() > 60) { + navbar.addClass(collapsed); + } + } + } + + // Search factory + function localSearch() { + console.log("using local search"); + var lunrIndex = lunr(function () { + this.ref('href'); + this.field('title', { boost: 50 }); + this.field('keywords', { boost: 20 }); + }); + lunr.tokenizer.seperator = /[\s\-\.]+/; + var searchData = {}; + var searchDataRequest = new XMLHttpRequest(); + + var indexPath = relHref + "index.json"; + if (indexPath) { + searchDataRequest.open('GET', indexPath); + searchDataRequest.onload = function () { + if (this.status != 200) { + return; + } + searchData = JSON.parse(this.responseText); + for (var prop in searchData) { + if (searchData.hasOwnProperty(prop)) { + lunrIndex.add(searchData[prop]); + } + } + } + searchDataRequest.send(); + } + + $("body").bind("queryReady", function () { + var hits = lunrIndex.search(query); + var results = []; + hits.forEach(function (hit) { + var item = searchData[hit.ref]; + results.push({ 'href': item.href, 'title': item.title, 'keywords': item.keywords }); + }); + handleSearchResults(results); + }); + } + + function webWorkerSearch() { + console.log("using Web Worker"); + var indexReady = $.Deferred(); + + worker.onmessage = function (oEvent) { + switch (oEvent.data.e) { + case 'index-ready': + indexReady.resolve(); + break; + case 'query-ready': + var hits = oEvent.data.d; + handleSearchResults(hits); + break; + } + } + + indexReady.promise().done(function () { + $("body").bind("queryReady", function () { + worker.postMessage({ q: query }); + }); + if (query && (query.length >= 3)) { + worker.postMessage({ q: query }); + } + }); + } + + // Highlight the searching keywords + function highlightKeywords() { + var q = url('?q'); + if (q) { + var keywords = q.split("%20"); + keywords.forEach(function (keyword) { + if (keyword !== "") { + $('.data-searchable *').mark(keyword); + $('article *').mark(keyword); + } + }); + } + } + + function addSearchEvent() { + $('body').bind("searchEvent", function () { + $('#search-query').keypress(function (e) { + return e.which !== 13; + }); + + $('#search-query').keyup(function () { + query = $(this).val(); + if (query.length < 3) { + flipContents("show"); + } else { + flipContents("hide"); + $("body").trigger("queryReady"); + $('#search-results>.search-list>span').text('"' + query + '"'); + } + }).off("keydown"); + }); + } + + function flipContents(action) { + if (action === "show") { + $('.hide-when-search').show(); + $('#search-results').hide(); + } else { + $('.hide-when-search').hide(); + $('#search-results').show(); + } + } + + function relativeUrlToAbsoluteUrl(currentUrl, relativeUrl) { + var currentItems = currentUrl.split(/\/+/); + var relativeItems = relativeUrl.split(/\/+/); + var depth = currentItems.length - 1; + var items = []; + for (var i = 0; i < relativeItems.length; i++) { + if (relativeItems[i] === '..') { + depth--; + } else if (relativeItems[i] !== '.') { + items.push(relativeItems[i]); + } + } + return currentItems.slice(0, depth).concat(items).join('/'); + } + + function extractContentBrief(content) { + var briefOffset = 512; + var words = query.split(/\s+/g); + var queryIndex = content.indexOf(words[0]); + var briefContent; + if (queryIndex > briefOffset) { + return "..." + content.slice(queryIndex - briefOffset, queryIndex + briefOffset) + "..."; + } else if (queryIndex <= briefOffset) { + return content.slice(0, queryIndex + briefOffset) + "..."; + } + } + + function handleSearchResults(hits) { + var numPerPage = 10; + var pagination = $('#pagination'); + pagination.empty(); + pagination.removeData("twbs-pagination"); + if (hits.length === 0) { + $('#search-results>.sr-items').html('

No results found

'); + } else { + pagination.twbsPagination({ + first: pagination.data('first'), + prev: pagination.data('prev'), + next: pagination.data('next'), + last: pagination.data('last'), + totalPages: Math.ceil(hits.length / numPerPage), + visiblePages: 5, + onPageClick: function (event, page) { + var start = (page - 1) * numPerPage; + var curHits = hits.slice(start, start + numPerPage); + $('#search-results>.sr-items').empty().append( + curHits.map(function (hit) { + var currentUrl = window.location.href; + var itemRawHref = relativeUrlToAbsoluteUrl(currentUrl, relHref + hit.href); + var itemHref = relHref + hit.href + "?q=" + query; + var itemTitle = hit.title; + var itemBrief = extractContentBrief(hit.keywords); + + var itemNode = $('
').attr('class', 'sr-item'); + var itemTitleNode = $('
').attr('class', 'item-title').append($('').attr('href', itemHref).attr("target", "_blank").attr("rel", "noopener noreferrer").text(itemTitle)); + var itemHrefNode = $('
').attr('class', 'item-href').text(itemRawHref); + var itemBriefNode = $('
').attr('class', 'item-brief').text(itemBrief); + itemNode.append(itemTitleNode).append(itemHrefNode).append(itemBriefNode); + return itemNode; + }) + ); + query.split(/\s+/).forEach(function (word) { + if (word !== '') { + $('#search-results>.sr-items *').mark(word); + } + }); + } + }); + } + } + }; + + // Update href in navbar + function renderNavbar() { + var navbar = $('#navbar ul')[0]; + if (typeof (navbar) === 'undefined') { + loadNavbar(); + } else { + $('#navbar ul a.active').parents('li').addClass(active); + renderBreadcrumb(); + showSearch(); + } + + function showSearch() { + if ($('#search-results').length !== 0) { + $('#search').show(); + $('body').trigger("searchEvent"); + } + } + + function loadNavbar() { + var navbarPath = $("meta[property='docfx\\:navrel']").attr("content"); + if (!navbarPath) { + return; + } + navbarPath = navbarPath.replace(/\\/g, '/'); + var tocPath = $("meta[property='docfx\\:tocrel']").attr("content") || ''; + if (tocPath) tocPath = tocPath.replace(/\\/g, '/'); + $.get(navbarPath, function (data) { + $(data).find("#toc>ul").appendTo("#navbar"); + showSearch(); + var index = navbarPath.lastIndexOf('/'); + var navrel = ''; + if (index > -1) { + navrel = navbarPath.substr(0, index + 1); + } + $('#navbar>ul').addClass('navbar-nav'); + var currentAbsPath = util.getCurrentWindowAbsolutePath(); + // set active item + $('#navbar').find('a[href]').each(function (i, e) { + var href = $(e).attr("href"); + if (util.isRelativePath(href)) { + href = navrel + href; + $(e).attr("href", href); + + var isActive = false; + var originalHref = e.name; + if (originalHref) { + originalHref = navrel + originalHref; + if (util.getDirectory(util.getAbsolutePath(originalHref)) === util.getDirectory(util.getAbsolutePath(tocPath))) { + isActive = true; + } + } else { + if (util.getAbsolutePath(href) === currentAbsPath) { + var dropdown = $(e).attr('data-toggle') == "dropdown" + if (!dropdown) { + isActive = true; + } + } + } + if (isActive) { + $(e).addClass(active); + } + } + }); + renderNavbar(); + }); + } + } + + function renderSidebar() { + var sidetoc = $('#sidetoggle .sidetoc')[0]; + if (typeof (sidetoc) === 'undefined') { + loadToc(); + } else { + registerTocEvents(); + if ($('footer').is(':visible')) { + $('.sidetoc').addClass('shiftup'); + } + + // Scroll to active item + var top = 0; + $('#toc a.active').parents('li').each(function (i, e) { + $(e).addClass(active).addClass(expanded); + $(e).children('a').addClass(active); + }) + $('#toc a.active').parents('li').each(function (i, e) { + top += $(e).position().top; + }) + $('.sidetoc').scrollTop(top - 50); + + if ($('footer').is(':visible')) { + $('.sidetoc').addClass('shiftup'); + } + + renderBreadcrumb(); + } + + function registerTocEvents() { + var tocFilterInput = $('#toc_filter_input'); + var tocFilterClearButton = $('#toc_filter_clear'); + + $('.toc .nav > li > .expand-stub').click(function (e) { + $(e.target).parent().toggleClass(expanded); + }); + $('.toc .nav > li > .expand-stub + a:not([href])').click(function (e) { + $(e.target).parent().toggleClass(expanded); + }); + tocFilterInput.on('input', function (e) { + var val = this.value; + //Save filter string to local session storage + if (typeof(Storage) !== "undefined") { + try { + sessionStorage.filterString = val; + } + catch(e) + {} + } + if (val === '') { + // Clear 'filtered' class + $('#toc li').removeClass(filtered).removeClass(hide); + tocFilterClearButton.fadeOut(); + return; + } + tocFilterClearButton.fadeIn(); + + // set all parent nodes status + $('#toc li>a').filter(function (i, e) { + return $(e).siblings().length > 0 + }).each(function (i, anchor) { + var parent = $(anchor).parent(); + parent.addClass(hide); + parent.removeClass(show); + parent.removeClass(filtered); + }) + + // Get leaf nodes + $('#toc li>a').filter(function (i, e) { + return $(e).siblings().length === 0 + }).each(function (i, anchor) { + var text = $(anchor).attr('title'); + var parent = $(anchor).parent(); + var parentNodes = parent.parents('ul>li'); + for (var i = 0; i < parentNodes.length; i++) { + var parentText = $(parentNodes[i]).children('a').attr('title'); + if (parentText) text = parentText + '.' + text; + }; + if (filterNavItem(text, val)) { + parent.addClass(show); + parent.removeClass(hide); + } else { + parent.addClass(hide); + parent.removeClass(show); + } + }); + $('#toc li>a').filter(function (i, e) { + return $(e).siblings().length > 0 + }).each(function (i, anchor) { + var parent = $(anchor).parent(); + if (parent.find('li.show').length > 0) { + parent.addClass(show); + parent.addClass(filtered); + parent.removeClass(hide); + } else { + parent.addClass(hide); + parent.removeClass(show); + parent.removeClass(filtered); + } + }) + + function filterNavItem(name, text) { + if (!text) return true; + if (name && name.toLowerCase().indexOf(text.toLowerCase()) > -1) return true; + return false; + } + }); + + // toc filter clear button + tocFilterClearButton.hide(); + tocFilterClearButton.on("click", function(e){ + tocFilterInput.val(""); + tocFilterInput.trigger('input'); + if (typeof(Storage) !== "undefined") { + try { + sessionStorage.filterString = ""; + } + catch(e) + {} + } + }); + + //Set toc filter from local session storage on page load + if (typeof(Storage) !== "undefined") { + try { + tocFilterInput.val(sessionStorage.filterString); + tocFilterInput.trigger('input'); + } + catch(e) + {} + } + } + + function loadToc() { + var tocPath = $("meta[property='docfx\\:tocrel']").attr("content"); + if (!tocPath) { + return; + } + tocPath = tocPath.replace(/\\/g, '/'); + $('#sidetoc').load(tocPath + " #sidetoggle > div", function () { + var index = tocPath.lastIndexOf('/'); + var tocrel = ''; + if (index > -1) { + tocrel = tocPath.substr(0, index + 1); + } + var currentHref = util.getCurrentWindowAbsolutePath(); + if(!currentHref.endsWith('.html')) { + currentHref += '.html'; + } + $('#sidetoc').find('a[href]').each(function (i, e) { + var href = $(e).attr("href"); + if (util.isRelativePath(href)) { + href = tocrel + href; + $(e).attr("href", href); + } + + if (util.getAbsolutePath(e.href) === currentHref) { + $(e).addClass(active); + } + + $(e).breakWord(); + }); + + renderSidebar(); + }); + } + } + + function renderBreadcrumb() { + var breadcrumb = []; + $('#navbar a.active').each(function (i, e) { + breadcrumb.push({ + href: e.href, + name: e.innerHTML + }); + }) + $('#toc a.active').each(function (i, e) { + breadcrumb.push({ + href: e.href, + name: e.innerHTML + }); + }) + + var html = util.formList(breadcrumb, 'breadcrumb'); + $('#breadcrumb').html(html); + } + + //Setup Affix + function renderAffix() { + var hierarchy = getHierarchy(); + if (!hierarchy || hierarchy.length <= 0) { + $("#affix").hide(); + } + else { + var html = util.formList(hierarchy, ['nav', 'bs-docs-sidenav']); + $("#affix>div").empty().append(html); + if ($('footer').is(':visible')) { + $(".sideaffix").css("bottom", "70px"); + } + $('#affix a').click(function(e) { + var scrollspy = $('[data-spy="scroll"]').data()['bs.scrollspy']; + var target = e.target.hash; + if (scrollspy && target) { + scrollspy.activate(target); + } + }); + } + + function getHierarchy() { + // supported headers are h1, h2, h3, and h4 + var $headers = $($.map(['h1', 'h2', 'h3', 'h4'], function (h) { return ".article article " + h; }).join(", ")); + + // a stack of hierarchy items that are currently being built + var stack = []; + $headers.each(function (i, e) { + if (!e.id) { + return; + } + + var item = { + name: htmlEncode($(e).text()), + href: "#" + e.id, + items: [] + }; + + if (!stack.length) { + stack.push({ type: e.tagName, siblings: [item] }); + return; + } + + var frame = stack[stack.length - 1]; + if (e.tagName === frame.type) { + frame.siblings.push(item); + } else if (e.tagName[1] > frame.type[1]) { + // we are looking at a child of the last element of frame.siblings. + // push a frame onto the stack. After we've finished building this item's children, + // we'll attach it as a child of the last element + stack.push({ type: e.tagName, siblings: [item] }); + } else { // e.tagName[1] < frame.type[1] + // we are looking at a sibling of an ancestor of the current item. + // pop frames from the stack, building items as we go, until we reach the correct level at which to attach this item. + while (e.tagName[1] < stack[stack.length - 1].type[1]) { + buildParent(); + } + if (e.tagName === stack[stack.length - 1].type) { + stack[stack.length - 1].siblings.push(item); + } else { + stack.push({ type: e.tagName, siblings: [item] }); + } + } + }); + while (stack.length > 1) { + buildParent(); + } + + function buildParent() { + var childrenToAttach = stack.pop(); + var parentFrame = stack[stack.length - 1]; + var parent = parentFrame.siblings[parentFrame.siblings.length - 1]; + $.each(childrenToAttach.siblings, function (i, child) { + parent.items.push(child); + }); + } + if (stack.length > 0) { + + var topLevel = stack.pop().siblings; + if (topLevel.length === 1) { // if there's only one topmost header, dump it + return topLevel[0].items; + } + return topLevel; + } + return undefined; + } + + function htmlEncode(str) { + if (!str) return str; + return str + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + } + + function htmlDecode(value) { + if (!str) return str; + return value + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&'); + } + + function cssEscape(str) { + // see: http://stackoverflow.com/questions/2786538/need-to-escape-a-special-character-in-a-jquery-selector-string#answer-2837646 + if (!str) return str; + return str + .replace(/[!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/g, "\\$&"); + } + } + + // Show footer + function renderFooter() { + initFooter(); + $(window).on("scroll", showFooterCore); + + function initFooter() { + if (needFooter()) { + shiftUpBottomCss(); + // $("footer").show(); + } else { + resetBottomCss(); + // $("footer").hide(); + } + } + + function showFooterCore() { + if (needFooter()) { + shiftUpBottomCss(); + // $("footer").fadeIn(); + } else { + resetBottomCss(); + // $("footer").fadeOut(); + } + } + + function needFooter() { + var scrollHeight = $(document).height(); + var scrollPosition = $(window).height() + $(window).scrollTop(); + return (scrollHeight - scrollPosition) < 1; + } + + function resetBottomCss() { + $(".sidetoc").removeClass("shiftup"); + $(".sideaffix").removeClass("shiftup"); + + $(".sidetoc").css({ "bottom": 0 }); + $(".sideaffix").css({ "bottom": 0 }); + } + + function shiftUpBottomCss() { + $(".sidetoc").addClass("shiftup"); + $(".sideaffix").addClass("shiftup"); + var footheight = $('footer').height(); + + + $(".sidetoc").css({ "bottom": footheight }); + $(".sideaffix").css({ "bottom": footheight }); + + + var footheight = $('footer').height(); + var sideberheight = $('.sidetoc.shiftup').height(); + var vh = (sideberheight - footheight)-120; + + console.log(vh); + + // $('.sidetoc.shiftup').css({ "height": vh + '%' }); + + } + } + + function renderLogo() { + // For LOGO SVG + // Replace SVG with inline SVG + // http://stackoverflow.com/questions/11978995/how-to-change-color-of-svg-image-using-css-jquery-svg-image-replacement + jQuery('img.svg').each(function () { + var $img = jQuery(this); + var imgID = $img.attr('id'); + var imgClass = $img.attr('class'); + var imgURL = $img.attr('src'); + + jQuery.get(imgURL, function (data) { + // Get the SVG tag, ignore the rest + var $svg = jQuery(data).find('svg'); + + // Add replaced image's ID to the new SVG + if (typeof imgID !== 'undefined') { + $svg = $svg.attr('id', imgID); + } + // Add replaced image's classes to the new SVG + if (typeof imgClass !== 'undefined') { + $svg = $svg.attr('class', imgClass + ' replaced-svg'); + } + + // Remove any invalid XML tags as per http://validator.w3.org + $svg = $svg.removeAttr('xmlns:a'); + + // Replace image with new SVG + $img.replaceWith($svg); + + }, 'xml'); + }); + } + + function renderTabs() { + var contentAttrs = { + id: 'data-bi-id', + name: 'data-bi-name', + type: 'data-bi-type' + }; + + var Tab = (function () { + function Tab(li, a, section) { + this.li = li; + this.a = a; + this.section = section; + } + Object.defineProperty(Tab.prototype, "tabIds", { + get: function () { return this.a.getAttribute('data-tab').split(' '); }, + enumerable: true, + configurable: true + }); + Object.defineProperty(Tab.prototype, "condition", { + get: function () { return this.a.getAttribute('data-condition'); }, + enumerable: true, + configurable: true + }); + Object.defineProperty(Tab.prototype, "visible", { + get: function () { return !this.li.hasAttribute('hidden'); }, + set: function (value) { + if (value) { + this.li.removeAttribute('hidden'); + this.li.removeAttribute('aria-hidden'); + } + else { + this.li.setAttribute('hidden', 'hidden'); + this.li.setAttribute('aria-hidden', 'true'); + } + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(Tab.prototype, "selected", { + get: function () { return !this.section.hasAttribute('hidden'); }, + set: function (value) { + if (value) { + this.a.setAttribute('aria-selected', 'true'); + this.a.tabIndex = 0; + this.section.removeAttribute('hidden'); + this.section.removeAttribute('aria-hidden'); + } + else { + this.a.setAttribute('aria-selected', 'false'); + this.a.tabIndex = -1; + this.section.setAttribute('hidden', 'hidden'); + this.section.setAttribute('aria-hidden', 'true'); + } + }, + enumerable: true, + configurable: true + }); + Tab.prototype.focus = function () { + this.a.focus(); + }; + return Tab; + }()); + + initTabs(document.body); + + function initTabs(container) { + var queryStringTabs = readTabsQueryStringParam(); + var elements = container.querySelectorAll('.tabGroup'); + var state = { groups: [], selectedTabs: [] }; + for (var i = 0; i < elements.length; i++) { + var group = initTabGroup(elements.item(i)); + if (!group.independent) { + updateVisibilityAndSelection(group, state); + state.groups.push(group); + } + } + container.addEventListener('click', function (event) { return handleClick(event, state); }); + if (state.groups.length === 0) { + return state; + } + selectTabs(queryStringTabs, container); + updateTabsQueryStringParam(state); + notifyContentUpdated(); + return state; + } + + function initTabGroup(element) { + var group = { + independent: element.hasAttribute('data-tab-group-independent'), + tabs: [] + }; + var li = element.firstElementChild.firstElementChild; + while (li) { + var a = li.firstElementChild; + a.setAttribute(contentAttrs.name, 'tab'); + var dataTab = a.getAttribute('data-tab').replace(/\+/g, ' '); + a.setAttribute('data-tab', dataTab); + var section = element.querySelector("[id=\"" + a.getAttribute('aria-controls') + "\"]"); + var tab = new Tab(li, a, section); + group.tabs.push(tab); + li = li.nextElementSibling; + } + element.setAttribute(contentAttrs.name, 'tab-group'); + element.tabGroup = group; + return group; + } + + function updateVisibilityAndSelection(group, state) { + var anySelected = false; + var firstVisibleTab; + for (var _i = 0, _a = group.tabs; _i < _a.length; _i++) { + var tab = _a[_i]; + tab.visible = tab.condition === null || state.selectedTabs.indexOf(tab.condition) !== -1; + if (tab.visible) { + if (!firstVisibleTab) { + firstVisibleTab = tab; + } + } + tab.selected = tab.visible && arraysIntersect(state.selectedTabs, tab.tabIds); + anySelected = anySelected || tab.selected; + } + if (!anySelected) { + for (var _b = 0, _c = group.tabs; _b < _c.length; _b++) { + var tabIds = _c[_b].tabIds; + for (var _d = 0, tabIds_1 = tabIds; _d < tabIds_1.length; _d++) { + var tabId = tabIds_1[_d]; + var index = state.selectedTabs.indexOf(tabId); + if (index === -1) { + continue; + } + state.selectedTabs.splice(index, 1); + } + } + var tab = firstVisibleTab; + tab.selected = true; + state.selectedTabs.push(tab.tabIds[0]); + } + } + + function getTabInfoFromEvent(event) { + if (!(event.target instanceof HTMLElement)) { + return null; + } + var anchor = event.target.closest('a[data-tab]'); + if (anchor === null) { + return null; + } + var tabIds = anchor.getAttribute('data-tab').split(' '); + var group = anchor.parentElement.parentElement.parentElement.tabGroup; + if (group === undefined) { + return null; + } + return { tabIds: tabIds, group: group, anchor: anchor }; + } + + function handleClick(event, state) { + var info = getTabInfoFromEvent(event); + if (info === null) { + return; + } + event.preventDefault(); + info.anchor.href = 'javascript:'; + setTimeout(function () { return info.anchor.href = '#' + info.anchor.getAttribute('aria-controls'); }); + var tabIds = info.tabIds, group = info.group; + var originalTop = info.anchor.getBoundingClientRect().top; + if (group.independent) { + for (var _i = 0, _a = group.tabs; _i < _a.length; _i++) { + var tab = _a[_i]; + tab.selected = arraysIntersect(tab.tabIds, tabIds); + } + } + else { + if (arraysIntersect(state.selectedTabs, tabIds)) { + return; + } + var previousTabId = group.tabs.filter(function (t) { return t.selected; })[0].tabIds[0]; + state.selectedTabs.splice(state.selectedTabs.indexOf(previousTabId), 1, tabIds[0]); + for (var _b = 0, _c = state.groups; _b < _c.length; _b++) { + var group_1 = _c[_b]; + updateVisibilityAndSelection(group_1, state); + } + updateTabsQueryStringParam(state); + } + notifyContentUpdated(); + var top = info.anchor.getBoundingClientRect().top; + if (top !== originalTop && event instanceof MouseEvent) { + window.scrollTo(0, window.pageYOffset + top - originalTop); + } + } + + function selectTabs(tabIds) { + for (var _i = 0, tabIds_1 = tabIds; _i < tabIds_1.length; _i++) { + var tabId = tabIds_1[_i]; + var a = document.querySelector(".tabGroup > ul > li > a[data-tab=\"" + tabId + "\"]:not([hidden])"); + if (a === null) { + return; + } + a.dispatchEvent(new CustomEvent('click', { bubbles: true })); + } + } + + function readTabsQueryStringParam() { + var qs = parseQueryString(window.location.search); + var t = qs.tabs; + if (t === undefined || t === '') { + return []; + } + return t.split(','); + } + + function updateTabsQueryStringParam(state) { + var qs = parseQueryString(window.location.search); + qs.tabs = state.selectedTabs.join(); + var url = location.protocol + "//" + location.host + location.pathname + "?" + toQueryString(qs) + location.hash; + if (location.href === url) { + return; + } + history.replaceState({}, document.title, url); + } + + function toQueryString(args) { + var parts = []; + for (var name_1 in args) { + if (args.hasOwnProperty(name_1) && args[name_1] !== '' && args[name_1] !== null && args[name_1] !== undefined) { + parts.push(encodeURIComponent(name_1) + '=' + encodeURIComponent(args[name_1])); + } + } + return parts.join('&'); + } + + function parseQueryString(queryString) { + var match; + var pl = /\+/g; + var search = /([^&=]+)=?([^&]*)/g; + var decode = function (s) { return decodeURIComponent(s.replace(pl, ' ')); }; + if (queryString === undefined) { + queryString = ''; + } + queryString = queryString.substring(1); + var urlParams = {}; + while (match = search.exec(queryString)) { + urlParams[decode(match[1])] = decode(match[2]); + } + return urlParams; + } + + function arraysIntersect(a, b) { + for (var _i = 0, a_1 = a; _i < a_1.length; _i++) { + var itemA = a_1[_i]; + for (var _a = 0, b_1 = b; _a < b_1.length; _a++) { + var itemB = b_1[_a]; + if (itemA === itemB) { + return true; + } + } + } + return false; + } + + function notifyContentUpdated() { + // Dispatch this event when needed + // window.dispatchEvent(new CustomEvent('content-update')); + } + } + + function utility() { + this.getAbsolutePath = getAbsolutePath; + this.isRelativePath = isRelativePath; + this.isAbsolutePath = isAbsolutePath; + this.getCurrentWindowAbsolutePath = getCurrentWindowAbsolutePath; + this.getDirectory = getDirectory; + this.formList = formList; + + function getAbsolutePath(href) { + if (isAbsolutePath(href)) return href; + var currentAbsPath = getCurrentWindowAbsolutePath(); + var stack = currentAbsPath.split("/"); + stack.pop(); + var parts = href.split("/"); + for (var i=0; i< parts.length; i++) { + if (parts[i] == ".") continue; + if (parts[i] == ".." && stack.length > 0) + stack.pop(); + else + stack.push(parts[i]); + } + var p = stack.join("/"); + return p; + } + + function isRelativePath(href) { + if (href === undefined || href === '' || href[0] === '/') { + return false; + } + return !isAbsolutePath(href); + } + + function isAbsolutePath(href) { + return (/^(?:[a-z]+:)?\/\//i).test(href); + } + + function getCurrentWindowAbsolutePath() { + return window.location.origin + window.location.pathname; + } + function getDirectory(href) { + if (!href) return ''; + var index = href.lastIndexOf('/'); + if (index == -1) return ''; + if (index > -1) { + return href.substr(0, index); + } + } + + function formList(item, classes) { + var level = 1; + var model = { + items: item + }; + var cls = [].concat(classes).join(" "); + return getList(model, cls); + + function getList(model, cls) { + if (!model || !model.items) return null; + var l = model.items.length; + if (l === 0) return null; + var html = '
    '; + level++; + for (var i = 0; i < l; i++) { + var item = model.items[i]; + var href = item.href; + var name = item.name; + if (!name) continue; + html += href ? '
  • ' + name + '' : '
  • ' + name; + html += getList(item, cls) || ''; + html += '
  • '; + } + html += '
'; + return html; + } + } + + /** + * Add into long word. + * @param {String} text - The word to break. It should be in plain text without HTML tags. + */ + function breakPlainText(text) { + if (!text) return text; + return text.replace(/([a-z])([A-Z])|(\.)(\w)/g, '$1$3$2$4') + } + + /** + * Add into long word. The jQuery element should contain no html tags. + * If the jQuery element contains tags, this function will not change the element. + */ + $.fn.breakWord = function () { + if (this.html() == this.text()) { + this.html(function (index, text) { + return breakPlainText(text); + }) + } + return this; + } + } + + // adjusted from https://stackoverflow.com/a/13067009/1523776 + function workAroundFixedHeaderForAnchors() { + var HISTORY_SUPPORT = !!(history && history.pushState); + var ANCHOR_REGEX = /^#[^ ]+$/; + + function getFixedOffset() { + return $('header').first().height(); + } + + /** + * If the provided href is an anchor which resolves to an element on the + * page, scroll to it. + * @param {String} href + * @return {Boolean} - Was the href an anchor. + */ + function scrollIfAnchor(href, pushToHistory) { + var match, rect, anchorOffset; + + if (!ANCHOR_REGEX.test(href)) { + return false; + } + + match = document.getElementById(href.slice(1)); + + if (match) { + rect = match.getBoundingClientRect(); + anchorOffset = window.pageYOffset + rect.top - getFixedOffset(); + window.scrollTo(window.pageXOffset, anchorOffset); + + // Add the state to history as-per normal anchor links + if (HISTORY_SUPPORT && pushToHistory) { + history.pushState({}, document.title, location.pathname + href); + } + } + + return !!match; + } + + /** + * Attempt to scroll to the current location's hash. + */ + function scrollToCurrent() { + scrollIfAnchor(window.location.hash); + } + + /** + * If the click event's target was an anchor, fix the scroll position. + */ + function delegateAnchors(e) { + var elem = e.target; + + if (scrollIfAnchor(elem.getAttribute('href'), true)) { + e.preventDefault(); + } + } + + $(window).on('hashchange', scrollToCurrent); + + $(window).on('load', function () { + // scroll to the anchor if present, offset by the header + scrollToCurrent(); + }); + + $(document).ready(function () { + // Exclude tabbed content case + $('a:not([data-tab])').click(function (e) { delegateAnchors(e); }); + }); + } +}); diff --git a/docs/template/styles/main.css b/docs/template/styles/main.css new file mode 100644 index 00000000000..f7e02841d11 --- /dev/null +++ b/docs/template/styles/main.css @@ -0,0 +1,365 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;300;400;500;600;700;800;900&family=Roboto+Mono:wght@100;200;300;400;500;600;700&display=swap'); + +body{ + color: #b5c3d7; + font-family: Inter; + font-size: 16px; + font-weight: 400; + line-height: 28px; + letter-spacing: 0.04442743px; + background-color: #0F172B; +} +#wrapper{ + background-color: #0F172B; +} +header .navbar{ + background-color: #0f172b; + border-bottom: 1px solid #2a2a2a; + background-image: url(/images/header.svg); + background-repeat: no-repeat; + background-position: 90% 0%; +} +.navbar-brand .svg{ + height: 60px; +} +header .subnav.navbar{ + display: none; +} + +.sidefilter{ + top: 60px; + background: transparent; + border-left: 0px; + border-right: 1px solid #2a2a2a; +} +.sidetoc{ + top: 120px; + background: none; + border-left: dashed 1px #2a2a2a; + border-right: dashed 1px #2a2a2a; + +} +.sidetoc .toc{background: none; overflow-y: hidden ;} +.toc .level2{ + margin-left: -10px; +} + +.toc .nav > li > a{ + padding: 5px 0px; + color: #94a3b8; + font-family: Inter; + font-size: 13px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.036097284px; +} +.toc .nav > li > a:hover, .toc .nav > li > a:focus { + color: #6dc9ed; +} +.toc .nav > li.active > a{ + color: #e2e8f0; + font-size: 15px; + font-weight: 600; + line-height: 18px; + letter-spacing: 0.041650712px; +} +.toc .nav > li ul > li{ + padding-left: 9px; +} + +.toc .nav > li> .expand-stub{ + color: #fff; +} +.toc .nav > li ul > li .expand-stub{ + left: 0px; +} +.toc .nav > li.active ul > li.active{ + border-left: 2px solid #6dc9ed; + background: #17243c; +} + +.toc .nav > li.active ul > li.active > a{ + color: #6dc9ed; + font-family: Inter; + font-size: 13px; + font-weight: 600; + line-height: 16px; + letter-spacing: 0.036097284px; +} + +.table-striped>tbody>tr:nth-of-type(odd) { + background-color: transparent !important; +} + +.article{ + margin-top: 90px; +} + +.navbar-inverse .navbar-nav>li>a, .navbar-inverse .navbar-text{ + color: #e2e8f0; + font-family: Inter; + font-size: 13px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.036097284px; + padding-top: 22px; + padding-bottom: 22px; +} +header .navbar .navbar-nav >li.active{ + border-bottom: 1px solid #6dc9ed; + +} + +.navbar-inverse .navbar-nav>.active>a, .navbar-inverse .navbar-nav>.active>a:focus, .navbar-inverse .navbar-nav>.active>a:hover { + color: #6dc9ed; + background-color: transparent; +} +.toc-filter{ + border-radius: 8px; + border: solid 2px #383838; + background: transparent; + color: #94a3b8; + font-family: Inter; + font-size: 13px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.036097284px; + +} +.toc-filter > input{ + color: #94a3b8; + font-family: Inter; + font-size: 13px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.036097284px; + background: transparent; +} +.affix{ + height: auto; +} +.affix h5 +{ + color: #e2e8f0; + font-family: Inter; + font-size: 14px; + font-weight: 600; + line-height: 17px; + letter-spacing: 0.038874px; +} +.affix ul > li > a +{ + color: #94a3b8; + font-family: Inter; + font-size: 14px; + font-weight: 500; + line-height: 17px; + letter-spacing: 0.038874px; +} +.affix ul > li.active > a, .affix ul > li.active > a:before{ + color: #6dc9ed; + font-family: Inter; + font-size: 14px; + font-weight: 500; + line-height: 17px; + letter-spacing: 0.038874px; +} +.level1.nav.bs-docs-sidenav{ + border-bottom: dashed 1px #2a2a2a; +} +.sideaffix .contribution { + margin-top: 10px; +} +.sideaffix .contribution .heading{ + color: #e2e8f0; + font-family: Inter; + font-size: 14px; + font-weight: 600; + line-height: 17px; + letter-spacing: 0.038874px; +} +.sideaffix .contribution .nav li a img{ + width: 16px; + margin-right: 10px; +} +.sideaffix .contribution .nav li a { + color: #e2e8f0; + font-family: Inter; + font-size: 14px; + font-weight: 600; + line-height: 17px; + letter-spacing: 0.038874px; + +} +.sideaffix > div.contribution > ul > li > a.contribution-link:hover{ + background-color: transparent; + color: #6dc9ed; +} + +.toc .nav > li.active > .expand-stub::before, .toc .nav > li.in > .expand-stub::before, .toc .nav > li.in.active > .expand-stub::before, .toc .nav > li.filtered > .expand-stub::before{ + content: "\e113"; + font-family:"Glyphicons Halflings"; + font-size: 12px; + font-weight: 400; +} +.toc .nav > li > .expand-stub::before, .toc .nav > li.active > .expand-stub::before{ + content: "\e114"; + font-family:"Glyphicons Halflings"; + font-size: 12px; + font-weight: 400; +} +article, article p{ + color: #b5c3d7; + font-family: Inter; + font-size: 16px; + font-weight: 400; + line-height: 28px; + letter-spacing: 0.04442743px; +} +article h1{ + color: #e2e8f0; + font-family: Inter; + font-size: 32px; + font-weight: 700; + line-height: 39px; + letter-spacing: 0.08885486px; +} +article h2 +{ + color: #e2e8f0; + font-family: Inter; + font-size: 24px; + font-weight: 700; + line-height: 29px; + letter-spacing: 0.066641144px; + +} + +ul li::marker{ + color: #547ea4; + font-size: 19px; +} +.article.grid-right{ + padding-bottom: 35px; +} +.sidetoc.shiftup{ + bottom: 0; +} + + +footer{ + position: relative !important; + opacity: 1 !important; + display: block !important; +} + +.footer{ + background: url(/images/footer.png) no-repeat left top, linear-gradient(270deg, #FFFFFF, #6DC9ED); + + color: #222322; + font-family: Inter; + font-size: 15px; + font-weight: 600; + line-height: 18px; + letter-spacing: 0.041650712px; + + padding: 20px 0; +} +.footer ul{ + margin: 10px 0px; + padding: 0px; +} +.footer ul li { + list-style: none; + margin: 10px 0px; +} + +.footer ul li a{ + color: #3f678b; + font-family: Inter; + font-size: 13px; + font-weight: 600; + line-height: 16px; + letter-spacing: 0.036097284px; +} +.footer ul li a:hover{ + color: #17243c; +} + +code{ + background: #0f172b; + color: #b5c3d7; + font-weight: 600; +} +pre{ + border-radius: 8px; + background: #040e1c; + padding: 15px; + color: #7dd3fc; + font-family: "Roboto Mono"; + font-size: 14px; + font-weight: 400; + line-height: 22px; +} +.hljs-attr, .hljs-selector-attr, .hljs-selector-class, .hljs-selector-id, .hljs-selector-pseudo, .hljs-title{ + color: #F472B6; +} +pre code{ + color: #E2E8F0 !important; +} + +.hljs-keyword { + color: #7DD3FC; +} +.alert p{color:#fff} +.alert-info { + color: #004085; + background-color: #cce5ff; + border-color: #b8daff; +} +.alert-info p{ + color: #004085; +} +.alert-info a{ + color: #25aae1; +} +.alert-warning { + background-color: #e0aa94; + border-color: #d69d85; +} +.alert-danger { + background-color: #f27f7f; + border-color: #fa7979; +} + + +.article .content.wrap img { + background: #25aae1; + padding: 10px; + border-radius: 6px; + +} + +@media (min-width: 768px) { + .navbar-brand{ + width: 260px; + } + .svg.akka-logo{ + margin: auto; + } +} + +@media only screen and (max-width: 768px){ + .content.wrap iframe{ + max-width: 100% !important; + } +} + +.pb-logo{ + width: 60px; + height: 60px; +} + +.jumbotron{ + background-color: #040e1c; +} \ No newline at end of file diff --git a/src/common.props b/src/common.props index d1a21be2d00..7e23057b9b7 100644 --- a/src/common.props +++ b/src/common.props @@ -15,7 +15,7 @@ 0.12.0 [12.0.3,) 2.0.1 - 3.17.3 + 3.19.4 0.13.1 netcoreapp3.1 net6.0 diff --git a/src/contrib/persistence/Akka.Persistence.Query.Sql/AllPersistenceIdsPublisher.cs b/src/contrib/persistence/Akka.Persistence.Query.Sql/AllPersistenceIdsPublisher.cs index 646131d0953..d4cf033bebc 100644 --- a/src/contrib/persistence/Akka.Persistence.Query.Sql/AllPersistenceIdsPublisher.cs +++ b/src/contrib/persistence/Akka.Persistence.Query.Sql/AllPersistenceIdsPublisher.cs @@ -136,7 +136,7 @@ protected override bool Receive(object message) { case Request _: _journalRef.Tell(new SelectCurrentPersistenceIds(0, Self)); - Become(Initializing); + Become(Waiting); return true; case Continue _: return true; @@ -148,7 +148,7 @@ protected override bool Receive(object message) } } - private bool Initializing(object message) + private bool Waiting(object message) { switch (message) { @@ -175,16 +175,12 @@ private bool Active(object message) { switch (message) { - case CurrentPersistenceIds added: - _lastOrderingOffset = added.HighestOrderingNumber; - _buffer.AddRange(added.AllPersistenceIds); - _buffer.DeliverBuffer(TotalDemand); - return true; case Request _: _buffer.DeliverBuffer(TotalDemand); return true; case Continue _: _journalRef.Tell(new SelectCurrentPersistenceIds(_lastOrderingOffset, Self)); + Become(Waiting); return true; case Cancel _: Context.Stop(Self); diff --git a/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/SqlJournal.cs b/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/SqlJournal.cs index 73f83383bac..f37855fdb36 100644 --- a/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/SqlJournal.cs +++ b/src/contrib/persistence/Akka.Persistence.Sql.Common/Journal/SqlJournal.cs @@ -106,7 +106,7 @@ protected override bool ReceivePluginInternal(object message) return true; case SelectCurrentPersistenceIds request: SelectAllPersistenceIdsAsync(request.Offset) - .PipeTo(request.ReplyTo, success: result => new CurrentPersistenceIds(result.Ids, request.Offset)); + .PipeTo(request.ReplyTo, success: result => new CurrentPersistenceIds(result.Ids, result.LastOrdering)); return true; case SubscribeTag subscribe: AddTagSubscriber(Sender, subscribe.Tag); diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteAllEventsSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteAllEventsSpec.cs index ab32bad34ad..a29f6e78327 100644 --- a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteAllEventsSpec.cs +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteAllEventsSpec.cs @@ -22,6 +22,7 @@ public class BatchingSqliteAllEventsSpec : AllEventsSpec public static Config Config => ConfigurationFactory.ParseString($@" akka.loglevel = INFO akka.persistence.journal.plugin = ""akka.persistence.journal.sqlite"" + akka.persistence.query.journal.sql.refresh-interval = 1s akka.persistence.journal.sqlite {{ class = ""Akka.Persistence.Sqlite.Journal.BatchingSqliteJournal, Akka.Persistence.Sqlite"" plugin-dispatcher = ""akka.actor.default-dispatcher"" @@ -29,7 +30,6 @@ class = ""Akka.Persistence.Sqlite.Journal.BatchingSqliteJournal, Akka.Persistenc metadata-table-name = journal_metadata auto-initialize = on connection-string = ""Filename=file:memdb-journal-eventsbytag-{Guid.NewGuid()}.db;Mode=Memory;Cache=Shared"" - refresh-interval = 1s }} akka.test.single-expect-default = 10s") .WithFallback(SqlReadJournal.DefaultConfiguration()); diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteCurrentAllEventsSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteCurrentAllEventsSpec.cs index 50bc2cdb881..57f72c811eb 100644 --- a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteCurrentAllEventsSpec.cs +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteCurrentAllEventsSpec.cs @@ -24,6 +24,7 @@ public class BatchingCurrentSqliteAllEventsSpec : CurrentAllEventsSpec public static Config Config(int id) => ConfigurationFactory.ParseString($@" akka.loglevel = INFO akka.persistence.journal.plugin = ""akka.persistence.journal.sqlite"" + akka.persistence.query.journal.sql.refresh-interval = 1s akka.persistence.journal.sqlite {{ class = ""Akka.Persistence.Sqlite.Journal.BatchingSqliteJournal, Akka.Persistence.Sqlite"" plugin-dispatcher = ""akka.actor.default-dispatcher"" @@ -31,7 +32,6 @@ class = ""Akka.Persistence.Sqlite.Journal.BatchingSqliteJournal, Akka.Persistenc metadata-table-name = journal_metadata auto-initialize = on connection-string = ""Filename=file:memdb-journal-eventsbytag-{id}.db;Mode=Memory;Cache=Shared"" - refresh-interval = 1s }} akka.test.single-expect-default = 10s") .WithFallback(SqlReadJournal.DefaultConfiguration()); diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteCurrentEventsByPersistenceIdSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteCurrentEventsByPersistenceIdSpec.cs index fcfb202fb11..31310ac7519 100644 --- a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteCurrentEventsByPersistenceIdSpec.cs +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteCurrentEventsByPersistenceIdSpec.cs @@ -20,6 +20,7 @@ public class BatchingSqliteCurrentEventsByPersistenceIdSpec : CurrentEventsByPer public static Config Config(int id) => ConfigurationFactory.ParseString($@" akka.loglevel = INFO akka.persistence.journal.plugin = ""akka.persistence.journal.sqlite"" + akka.persistence.query.journal.sql.refresh-interval = 1s akka.persistence.journal.sqlite {{ class = ""Akka.Persistence.Sqlite.Journal.BatchingSqliteJournal, Akka.Persistence.Sqlite"" plugin-dispatcher = ""akka.actor.default-dispatcher"" @@ -27,7 +28,6 @@ class = ""Akka.Persistence.Sqlite.Journal.BatchingSqliteJournal, Akka.Persistenc metadata-table-name = journal_metadata auto-initialize = on connection-string = ""Datasource=memdb-journal-batch-currenteventsbypersistenceid-{id}.db;Mode=Memory;Cache=Shared"" - refresh-interval = 1s }} akka.test.single-expect-default = 10s") .WithFallback(SqlReadJournal.DefaultConfiguration()); diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteCurrentEventsByTagSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteCurrentEventsByTagSpec.cs index c6026f341e1..7c39fdec26f 100644 --- a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteCurrentEventsByTagSpec.cs +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteCurrentEventsByTagSpec.cs @@ -21,6 +21,7 @@ public class BatchingSqliteCurrentEventsByTagSpec : CurrentEventsByTagSpec public static Config Config(int id) => ConfigurationFactory.ParseString($@" akka.loglevel = INFO akka.persistence.journal.plugin = ""akka.persistence.journal.sqlite"" + akka.persistence.query.journal.sql.refresh-interval = 1s akka.persistence.journal.sqlite {{ event-adapters {{ color-tagger = ""Akka.Persistence.TCK.Query.ColorFruitTagger, Akka.Persistence.TCK"" @@ -34,7 +35,6 @@ class = ""Akka.Persistence.Sqlite.Journal.BatchingSqliteJournal, Akka.Persistenc metadata-table-name = journal_metadata auto-initialize = on connection-string = ""Datasource=memdb-journal-batch-currenteventsbytag-{id}.db;Mode=Memory;Cache=Shared"" - refresh-interval = 1s }} akka.test.single-expect-default = 10s") .WithFallback(SqlReadJournal.DefaultConfiguration()); diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteCurrentPersistenceIdsSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteCurrentPersistenceIdsSpec.cs index 74bdf202a4a..5a99891c402 100644 --- a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteCurrentPersistenceIdsSpec.cs +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteCurrentPersistenceIdsSpec.cs @@ -21,6 +21,7 @@ public class BatchingSqliteCurrentPersistenceIdsSpec : CurrentPersistenceIdsSpec public static Config Config(int id) => ConfigurationFactory.ParseString($@" akka.loglevel = INFO akka.persistence.journal.plugin = ""akka.persistence.journal.sqlite"" + akka.persistence.query.journal.sql.refresh-interval = 1s akka.persistence.journal.sqlite {{ class = ""Akka.Persistence.Sqlite.Journal.BatchingSqliteJournal, Akka.Persistence.Sqlite"" plugin-dispatcher = ""akka.actor.default-dispatcher"" @@ -28,7 +29,6 @@ class = ""Akka.Persistence.Sqlite.Journal.BatchingSqliteJournal, Akka.Persistenc metadata-table-name = journal_metadata auto-initialize = on connection-string = ""Datasource=memdb-journal-batch-currentpersistenceids-{id}.db;Mode=Memory;Cache=Shared"" - refresh-interval = 1s }} akka.test.single-expect-default = 10s") .WithFallback(SqlReadJournal.DefaultConfiguration()); diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteEventsByPersistenceIdSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteEventsByPersistenceIdSpec.cs index bc409376071..21912541b10 100644 --- a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteEventsByPersistenceIdSpec.cs +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteEventsByPersistenceIdSpec.cs @@ -20,6 +20,7 @@ public class BatchingSqliteEventsByPersistenceIdSpec : EventsByPersistenceIdSpec public static Config Config(int id) => ConfigurationFactory.ParseString($@" akka.loglevel = INFO akka.persistence.journal.plugin = ""akka.persistence.journal.sqlite"" + akka.persistence.query.journal.sql.refresh-interval = 1s akka.persistence.journal.sqlite {{ class = ""Akka.Persistence.Sqlite.Journal.BatchingSqliteJournal, Akka.Persistence.Sqlite"" plugin-dispatcher = ""akka.actor.default-dispatcher"" @@ -27,7 +28,6 @@ class = ""Akka.Persistence.Sqlite.Journal.BatchingSqliteJournal, Akka.Persistenc metadata-table-name = journal_metadata auto-initialize = on connection-string = ""Datasource=memdb-journal-batch-eventsbypersistenceid-{id}.db;Mode=Memory;Cache=Shared"" - refresh-interval = 1s }} akka.test.single-expect-default = 10s") .WithFallback(SqlReadJournal.DefaultConfiguration()); diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteEventsByTagSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteEventsByTagSpec.cs index 4bfb9d0b7da..6007ce6392a 100644 --- a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteEventsByTagSpec.cs +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqliteEventsByTagSpec.cs @@ -20,6 +20,7 @@ public class BatchingSqliteEventsByTagSpec : EventsByTagSpec public static Config Config(int id) => ConfigurationFactory.ParseString($@" akka.loglevel = INFO akka.persistence.journal.plugin = ""akka.persistence.journal.sqlite"" + akka.persistence.query.journal.sql.refresh-interval = 1s akka.persistence.journal.sqlite {{ event-adapters {{ color-tagger = ""Akka.Persistence.TCK.Query.ColorFruitTagger, Akka.Persistence.TCK"" @@ -33,7 +34,6 @@ class = ""Akka.Persistence.Sqlite.Journal.BatchingSqliteJournal, Akka.Persistenc metadata-table-name = journal_metadata auto-initialize = on connection-string = ""Datasource=memdb-journal-batch-eventsbytag-{id}.db;Mode=Memory;Cache=Shared"" - refresh-interval = 1s }} akka.test.single-expect-default = 10s") .WithFallback(SqlReadJournal.DefaultConfiguration()); diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqlitePersistenceIdSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqlitePersistenceIdSpec.cs index e32f8c4801c..4a5ba9cf28e 100644 --- a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqlitePersistenceIdSpec.cs +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Batching/BatchingSqlitePersistenceIdSpec.cs @@ -31,6 +31,7 @@ public class BatchingSqlitePersistenceIdSpec : PersistenceIdsSpec }} akka.persistence {{ publish-plugin-commands = on + query.journal.sql.refresh-interval = 200ms journal {{ plugin = ""akka.persistence.journal.sqlite"" sqlite = {{ @@ -40,7 +41,6 @@ class = ""Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite metadata-table-name = journal_metadata auto-initialize = on connection-string = ""{ConnectionString("journal")}"" - refresh-interval = 200ms }} }} snapshot-store {{ diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteAllEventsSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteAllEventsSpec.cs index ec4fed0fcf8..72f106761d5 100644 --- a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteAllEventsSpec.cs +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteAllEventsSpec.cs @@ -22,6 +22,7 @@ public class SqliteAllEventsSpec:AllEventsSpec public static Config Config => ConfigurationFactory.ParseString($@" akka.loglevel = INFO akka.persistence.journal.plugin = ""akka.persistence.journal.sqlite"" + akka.persistence.query.journal.sql.refresh-interval = 1s akka.persistence.journal.sqlite {{ class = ""Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite"" plugin-dispatcher = ""akka.actor.default-dispatcher"" @@ -29,7 +30,6 @@ class = ""Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite metadata-table-name = journal_metadata auto-initialize = on connection-string = ""Filename=file:memdb-journal-eventsbytag-{Guid.NewGuid()}.db;Mode=Memory;Cache=Shared"" - refresh-interval = 1s }} akka.test.single-expect-default = 10s") .WithFallback(SqlReadJournal.DefaultConfiguration()); diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteCurrentAllEventsSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteCurrentAllEventsSpec.cs index e3d3bf99073..0861381201d 100644 --- a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteCurrentAllEventsSpec.cs +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteCurrentAllEventsSpec.cs @@ -24,6 +24,7 @@ public class SqliteCurrentAllEventsSpec:CurrentAllEventsSpec public static Config Config(int id) => ConfigurationFactory.ParseString($@" akka.loglevel = INFO akka.persistence.journal.plugin = ""akka.persistence.journal.sqlite"" + akka.persistence.query.journal.sql.refresh-interval = 1s akka.persistence.journal.sqlite {{ class = ""Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite"" plugin-dispatcher = ""akka.actor.default-dispatcher"" @@ -31,7 +32,6 @@ class = ""Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite metadata-table-name = journal_metadata auto-initialize = on connection-string = ""Filename=file:memdb-journal-allevents-{id}.db;Mode=Memory;Cache=Shared"" - refresh-interval = 1s }} akka.test.single-expect-default = 10s") .WithFallback(SqlReadJournal.DefaultConfiguration()); diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteCurrentEventsByPersistenceIdSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteCurrentEventsByPersistenceIdSpec.cs index 8c80aa1cb6d..86f0a868249 100644 --- a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteCurrentEventsByPersistenceIdSpec.cs +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteCurrentEventsByPersistenceIdSpec.cs @@ -20,6 +20,7 @@ public class SqliteCurrentEventsByPersistenceIdSpec : CurrentEventsByPersistence public static Config Config(int id) => ConfigurationFactory.ParseString($@" akka.loglevel = INFO akka.persistence.journal.plugin = ""akka.persistence.journal.sqlite"" + akka.persistence.query.journal.sql.refresh-interval = 1s akka.persistence.journal.sqlite {{ class = ""Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite"" plugin-dispatcher = ""akka.actor.default-dispatcher"" @@ -27,7 +28,6 @@ class = ""Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite metadata-table-name = journal_metadata auto-initialize = on connection-string = ""Filename=file:memdb-journal-currenteventsbypersistenceid-{id}.db;Mode=Memory;Cache=Shared"" - refresh-interval = 1s }} akka.test.single-expect-default = 10s") .WithFallback(SqlReadJournal.DefaultConfiguration()); diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteCurrentEventsByTagSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteCurrentEventsByTagSpec.cs index 50eaac2ae30..10155a66c8d 100644 --- a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteCurrentEventsByTagSpec.cs +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteCurrentEventsByTagSpec.cs @@ -22,6 +22,7 @@ public class SqliteCurrentEventsByTagSpec : CurrentEventsByTagSpec public static Config Config(int id) => ConfigurationFactory.ParseString($@" akka.loglevel = INFO akka.persistence.journal.plugin = ""akka.persistence.journal.sqlite"" + akka.persistence.query.journal.sql.refresh-interval = 1s akka.persistence.journal.sqlite {{ event-adapters {{ color-tagger = ""Akka.Persistence.TCK.Query.ColorFruitTagger, Akka.Persistence.TCK"" @@ -35,7 +36,6 @@ class = ""Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite metadata-table-name = journal_metadata auto-initialize = on connection-string = ""Filename=file:memdb-journal-currenteventsbytag-{id}.db;Mode=Memory;Cache=Shared"" - refresh-interval = 1s }} akka.test.single-expect-default = 10s") .WithFallback(SqlReadJournal.DefaultConfiguration()); diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteCurrentPersistenceIdsSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteCurrentPersistenceIdsSpec.cs index 1dedcc44ab9..5c73aec2aad 100644 --- a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteCurrentPersistenceIdsSpec.cs +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteCurrentPersistenceIdsSpec.cs @@ -22,6 +22,7 @@ public class SqliteCurrentPersistenceIdsSpec : CurrentPersistenceIdsSpec public static Config Config(int id) => ConfigurationFactory.ParseString($@" akka.loglevel = INFO akka.persistence.journal.plugin = ""akka.persistence.journal.sqlite"" + akka.persistence.query.journal.sql.refresh-interval = 1s akka.persistence.journal.sqlite {{ class = ""Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite"" plugin-dispatcher = ""akka.actor.default-dispatcher"" @@ -29,7 +30,6 @@ class = ""Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite metadata-table-name = journal_metadata auto-initialize = on connection-string = ""Filename=file:memdb-journal-currentpersistenceids-{id}.db;Mode=Memory;Cache=Shared"" - refresh-interval = 1s }} akka.test.single-expect-default = 10s") .WithFallback(SqlReadJournal.DefaultConfiguration()); diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteEventsByPersistenceIdSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteEventsByPersistenceIdSpec.cs index b97d6ac5017..c6f26ceb895 100644 --- a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteEventsByPersistenceIdSpec.cs +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteEventsByPersistenceIdSpec.cs @@ -20,6 +20,7 @@ public class SqliteEventsByPersistenceIdSpec : EventsByPersistenceIdSpec public static Config Config(int id) => ConfigurationFactory.ParseString($@" akka.loglevel = INFO akka.persistence.journal.plugin = ""akka.persistence.journal.sqlite"" + akka.persistence.query.journal.sql.refresh-interval = 1s akka.persistence.journal.sqlite {{ class = ""Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite"" plugin-dispatcher = ""akka.actor.default-dispatcher"" @@ -27,7 +28,6 @@ class = ""Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite metadata-table-name = journal_metadata auto-initialize = on connection-string = ""Filename=file:memdb-journal-eventsbypersistenceid-{id}.db;Mode=Memory;Cache=Shared"" - refresh-interval = 1s }} akka.test.single-expect-default = 10s") .WithFallback(SqlReadJournal.DefaultConfiguration()); diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteEventsByTagSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteEventsByTagSpec.cs index c713915d711..493c4a4289d 100644 --- a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteEventsByTagSpec.cs +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqliteEventsByTagSpec.cs @@ -21,6 +21,7 @@ public class SqliteEventsByTagSpec : EventsByTagSpec public static Config Config(int id) => ConfigurationFactory.ParseString($@" akka.loglevel = INFO + akka.persistence.query.journal.sql.refresh-interval = 1s akka.persistence.journal.plugin = ""akka.persistence.journal.sqlite"" akka.persistence.journal.sqlite {{ event-adapters {{ @@ -35,7 +36,6 @@ class = ""Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite metadata-table-name = journal_metadata auto-initialize = on connection-string = ""Filename=file:memdb-journal-eventsbytag-{id}.db;Mode=Memory;Cache=Shared"" - refresh-interval = 1s }} akka.test.single-expect-default = 10s") .WithFallback(SqlReadJournal.DefaultConfiguration()); diff --git a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqlitePersistenceIdsSpec.cs b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqlitePersistenceIdsSpec.cs index f7863993361..89e01ff218e 100644 --- a/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqlitePersistenceIdsSpec.cs +++ b/src/contrib/persistence/Akka.Persistence.Sqlite.Tests/Query/SqlitePersistenceIdsSpec.cs @@ -6,12 +6,15 @@ //----------------------------------------------------------------------- using System; +using System.Threading; using Akka.Actor; using Akka.Configuration; using Akka.Persistence.Query; using Akka.Persistence.Query.Sql; using Akka.Persistence.TCK.Query; +using Akka.Streams.TestKit; using Akka.Util.Internal; +using Xunit; using Xunit.Abstractions; namespace Akka.Persistence.Sqlite.Tests.Query @@ -32,6 +35,7 @@ public class SqlitePersistenceIdsSpec : PersistenceIdsSpec }} akka.persistence {{ publish-plugin-commands = on + query.journal.sql.refresh-interval = 200ms journal {{ plugin = ""akka.persistence.journal.sqlite"" sqlite = {{ @@ -41,7 +45,6 @@ class = ""Akka.Persistence.Sqlite.Journal.SqliteJournal, Akka.Persistence.Sqlite metadata-table-name = journal_metadata auto-initialize = on connection-string = ""{ConnectionString("journal")}"" - refresh-interval = 200ms }} }} snapshot-store {{ diff --git a/src/contrib/testkits/Akka.TestKit.Xunit2/Internals/Loggers.cs b/src/contrib/testkits/Akka.TestKit.Xunit2/Internals/Loggers.cs index 4ea59da9e0d..cfefe9758de 100644 --- a/src/contrib/testkits/Akka.TestKit.Xunit2/Internals/Loggers.cs +++ b/src/contrib/testkits/Akka.TestKit.Xunit2/Internals/Loggers.cs @@ -49,12 +49,17 @@ private void HandleLogEvent(LogEvent e) { var message = $"Received a malformed formatted message. Log level: [{e.LogLevel()}], Template: [{msg.Format}], args: [{string.Join(",", msg.Args)}]"; - if(e.Cause != null) + if (e.Cause != null) throw new AggregateException(message, ex, e.Cause); throw new FormatException(message, ex); } + throw; } + catch (InvalidOperationException ie) + { + Console.WriteLine($"Received InvalidOperationException: {ie} - probably because the test had completed executing."); + } } } } diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index 08805ed01dd..3e3a0702e48 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -3044,6 +3044,7 @@ namespace Akka.Event public class EventStream : Akka.Event.LoggingBus { public EventStream(bool debug) { } + [System.ObsoleteAttribute("Should be removed in 1.5")] public bool InitUnsubscriber(Akka.Actor.IActorRef unsubscriber, Akka.Actor.ActorSystem system) { } public void StartUnsubscriber(Akka.Actor.Internal.ActorSystemImpl system) { } public override bool Subscribe(Akka.Actor.IActorRef subscriber, System.Type channel) { } diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt index 6af1dcc0ee5..aca25f5b718 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt @@ -1437,6 +1437,7 @@ namespace Akka.Streams.Dsl public sealed class FlowWithContext : Akka.Streams.GraphDelegate, System.ValueTuple>, TMat> { public Akka.Streams.Dsl.Flow, System.ValueTuple, TMat> AsFlow() { } + public Akka.Streams.Dsl.FlowWithContext MapMaterializedValue(System.Func combine) { } public Akka.Streams.Dsl.FlowWithContext Via(Akka.Streams.IGraph, System.ValueTuple>, TMat2> viaFlow) { } public Akka.Streams.Dsl.FlowWithContext ViaMaterialized(Akka.Streams.IGraph, System.ValueTuple>, TMat2> viaFlow, System.Func combine) { } } @@ -2106,7 +2107,10 @@ namespace Akka.Streams.Dsl { public SourceWithContext(Akka.Streams.Dsl.Source, TMat> source) { } public Akka.Streams.Dsl.Source, TMat> AsSource() { } + public Akka.Streams.Dsl.SourceWithContext MapMaterializedValue(System.Func combine) { } public TMat2 RunWith(Akka.Streams.IGraph>, TMat2> sink, Akka.Streams.IMaterializer materializer) { } + public Akka.Streams.Dsl.IRunnableGraph To(Akka.Streams.IGraph>, TMat2> sink) { } + public Akka.Streams.Dsl.IRunnableGraph ToMaterialized(Akka.Streams.IGraph>, TMat2> sink, System.Func combine) { } public Akka.Streams.Dsl.SourceWithContext Via(Akka.Streams.IGraph, System.ValueTuple>, TMat2> viaFlow) { } public Akka.Streams.Dsl.SourceWithContext ViaMaterialized(Akka.Streams.IGraph, System.ValueTuple>, TMat2> viaFlow, System.Func combine) { } } diff --git a/src/core/Akka.Persistence.TCK/Query/PersistenceIdsSpec.cs b/src/core/Akka.Persistence.TCK/Query/PersistenceIdsSpec.cs index 0a16b1099a8..daf054672c5 100644 --- a/src/core/Akka.Persistence.TCK/Query/PersistenceIdsSpec.cs +++ b/src/core/Akka.Persistence.TCK/Query/PersistenceIdsSpec.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using Akka.Actor; using Akka.Configuration; @@ -16,6 +17,7 @@ using Akka.Streams.TestKit; using Akka.TestKit; using Akka.Util.Internal; +using FluentAssertions; using Reactive.Streams; using Xunit; using Xunit.Abstractions; @@ -76,18 +78,23 @@ public virtual void ReadJournal_AllPersistenceIds_should_find_new_events_after_d var source = queries.PersistenceIds(); var probe = source.RunWith(this.SinkProbe(), Materializer); + var expected = new List { "h", "i", "j" }; probe.Within(TimeSpan.FromSeconds(10), () => { - probe.Request(1).ExpectNext(); - return probe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + expected.Remove(probe.Request(1).ExpectNext()).Should().BeTrue(); + return probe.ExpectNoMsg(TimeSpan.FromMilliseconds(500)); }); Setup("j", 1); - probe.Within(TimeSpan.FromSeconds(10), () => - { - probe.Request(5).ExpectNext(); - return probe.ExpectNext(); - }); + probe.Within(TimeSpan.FromSeconds(10), () => probe.Request(5).ExpectNextUnordered(expected[0], expected[1])); + + Setup("a1", 1); + Thread.Sleep(TimeSpan.FromSeconds(2)); + probe.ExpectNext(TimeSpan.FromSeconds(10)); + + Thread.Sleep(TimeSpan.FromSeconds(2)); + Setup("a2", 1); + probe.ExpectNext(TimeSpan.FromSeconds(10)); } [Fact] diff --git a/src/core/Akka.Streams.Tests/Dsl/FlowWithContextSpec.cs b/src/core/Akka.Streams.Tests/Dsl/FlowWithContextSpec.cs index 8a9d0858fb4..21116c20d05 100644 --- a/src/core/Akka.Streams.Tests/Dsl/FlowWithContextSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/FlowWithContextSpec.cs @@ -40,6 +40,23 @@ public void A_FlowWithContext_must_get_created_from_FlowAsFlowWithContext() .ExpectNext((new Message("az", 1L), 1L)) .ExpectComplete(); } + + [Fact] + public void A_FlowWithContext_must_be_able_to_map_materialized_value_via_FlowWithContext_MapMaterializedValue() + { + var materializedValue = "MatedValue"; + var mapMaterializedValueFlow = FlowWithContext.Create().MapMaterializedValue(_ => materializedValue); + + var (matValue, probe) = Source.From(new[] { new Message("a", 1L) }) + .MapMaterializedValue(_ => 42) + .AsSourceWithContext(m => m.Offset) + .ViaMaterialized(mapMaterializedValueFlow, Keep.Both) + .ToMaterialized(this.SinkProbe<(Message, long)>(), Keep.Both) + .Run(Materializer); + + matValue.ShouldBe((42, materializedValue)); + probe.Request(1).ExpectNext((new Message("a", 1L), 1L)).ExpectComplete(); + } } sealed class Message : IEquatable diff --git a/src/core/Akka.Streams.Tests/Dsl/SourceWithContextSpec.cs b/src/core/Akka.Streams.Tests/Dsl/SourceWithContextSpec.cs index 60c7a849443..97a1b52c859 100644 --- a/src/core/Akka.Streams.Tests/Dsl/SourceWithContextSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/SourceWithContextSpec.cs @@ -65,17 +65,13 @@ public void SourceWithContext_must_get_created_from_AsSourceWithContext() { var msg = new Message("a", 1); - var sink = this.CreateSubscriberProbe<(Message, long)>(); - Source.From(new[] { msg }) .AsSourceWithContext(x => x.Offset) - .AsSource() - .RunWith(Sink.FromSubscriber(sink), Materializer); - - var sub = sink.ExpectSubscription(); - sub.Request(1); - sink.ExpectNext((msg, 1L)); - sink.ExpectComplete(); + .ToMaterialized(this.SinkProbe<(Message, long)>(), Keep.Right) + .Run(Materializer) + .Request(1) + .ExpectNext((msg, 1L)) + .ExpectComplete(); } [Fact] @@ -100,8 +96,6 @@ public void SourceWithContext_must_be_able_to_get_turned_back_into_a_normal_sour [Fact] public void SourceWithContext_must_pass_through_context_using_Select_and_Where() { - var sink = this.CreateSubscriberProbe<(string, long)>(); - Source.From(new[] { new Message("A", 1), @@ -113,14 +107,12 @@ public void SourceWithContext_must_pass_through_context_using_Select_and_Where() .Select(m => m.Data.ToLower()) .Where(x => x != "b") .WhereNot(x => x == "d") - .AsSource() - .RunWith(Sink.FromSubscriber(sink), Materializer); - - var sub = sink.ExpectSubscription(); - sub.Request(2); - sink.ExpectNext(("a", 1L)); - sink.ExpectNext(("c", 4L)); - sink.ExpectComplete(); + .ToMaterialized(this.SinkProbe<(string, long)>(), Keep.Right) + .Run(Materializer) + .Request(2) + .ExpectNext(("a", 1L)) + .ExpectNext(("c", 4L)) + .ExpectComplete(); } [Fact] @@ -191,5 +183,18 @@ public void SourceWithContext_must_pass_through_sequence_of_context_per_element_ sink.ExpectComplete(); } + + [Fact] + public void SourceWithContext_must_be_able_to_change_materialized_value_via_MapMaterializedValue() + { + var materializedValue = "MatedValue"; + + Source.Empty() + .AsSourceWithContext(m => m.Offset) + .MapMaterializedValue(_ => materializedValue) + .To(Sink.Ignore<(Message, long)>()) + .Run(Materializer) + .ShouldBe(materializedValue); + } } } diff --git a/src/core/Akka.Streams/Dsl/FlowWithContext.cs b/src/core/Akka.Streams/Dsl/FlowWithContext.cs index 359295483b4..3a46e24c045 100644 --- a/src/core/Akka.Streams/Dsl/FlowWithContext.cs +++ b/src/core/Akka.Streams/Dsl/FlowWithContext.cs @@ -53,6 +53,12 @@ public FlowWithContext ViaMaterialized, TMat2> viaFlow, Func combine) => FlowWithContext.From(Flow.FromGraph(Inner).ViaMaterialized(viaFlow, combine)); + /// + /// Context-preserving variant of . + /// + public FlowWithContext MapMaterializedValue(Func combine) => + FlowWithContext.From(Flow.FromGraph(Inner).MapMaterializedValue(combine)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Flow<(TIn, TCtxIn), (TOut, TCtxOut), TMat> AsFlow() => Flow.FromGraph(Inner); } diff --git a/src/core/Akka.Streams/Dsl/SourceWithContext.cs b/src/core/Akka.Streams/Dsl/SourceWithContext.cs index d3e011cc2a4..2284f508100 100644 --- a/src/core/Akka.Streams/Dsl/SourceWithContext.cs +++ b/src/core/Akka.Streams/Dsl/SourceWithContext.cs @@ -48,6 +48,26 @@ public SourceWithContext ViaMaterialized, TMat2> viaFlow, Func combine) => new SourceWithContext(Source.FromGraph(Inner).ViaMaterialized(viaFlow, combine)); + /// + /// Connect this to a , + /// concatenating the processing steps of both. + /// + public IRunnableGraph To(IGraph, TMat2> sink) => + Source.FromGraph(Inner).ToMaterialized(sink, Keep.Left); + + /// + /// Connect this to a , + /// concatenating the processing steps of both. + /// + public IRunnableGraph ToMaterialized(IGraph, TMat2> sink, Func combine) => + Source.FromGraph(Inner).ToMaterialized(sink, combine); + + /// + /// Context-preserving variant of . + /// + public SourceWithContext MapMaterializedValue(Func combine) => + new SourceWithContext(Source.FromGraph(Inner).MapMaterializedValue(combine)); + /// /// Connect this to a Sink and run it. The returned value is the materialized value of the Sink. /// Note that the ActorSystem can be used as the implicit materializer parameter to use the SystemMaterializer for running the stream. diff --git a/src/core/Akka.Tests/Actor/Setup/ActorSystemSetupSpec.cs b/src/core/Akka.Tests/Actor/Setup/ActorSystemSetupSpec.cs index db856722bd0..7306ed0d6fa 100644 --- a/src/core/Akka.Tests/Actor/Setup/ActorSystemSetupSpec.cs +++ b/src/core/Akka.Tests/Actor/Setup/ActorSystemSetupSpec.cs @@ -6,6 +6,7 @@ //----------------------------------------------------------------------- using System; +using System.Collections.Generic; using Akka.Actor; using Akka.Actor.Setup; using Akka.TestKit; @@ -107,5 +108,36 @@ public void ActorSystemSettingsShouldBeAvailableFromExtendedActorSystem() system?.Terminate().Wait(TimeSpan.FromSeconds(5)); } } + + /// + /// Reproduction for https://github.com/akkadotnet/akka.net/issues/5728 + /// + [Fact] + public void ActorSystemSetupBugFix5728Reproduction() + { + // arrange + var setups = new HashSet(); + setups.Add(new DummySetup("Blantons")); + setups.Add(new DummySetup2("Colonel E.H. Taylor")); + + var actorSystemSetup = ActorSystemSetup.Empty; + + foreach (var s in setups) + { + actorSystemSetup = actorSystemSetup.And(s); + } + + // act + var dummySetup = actorSystemSetup.Get(); + var dummySetup2 = actorSystemSetup.Get(); + + // shouldn't exist + var dummySetup3 = actorSystemSetup.Get(); + + // assert + dummySetup.HasValue.Should().BeTrue(); + dummySetup2.HasValue.Should().BeTrue(); + dummySetup3.HasValue.Should().BeFalse(); + } } } diff --git a/src/core/Akka.Tests/Event/Bugfix5717Specs.cs b/src/core/Akka.Tests/Event/Bugfix5717Specs.cs new file mode 100644 index 00000000000..ee2480f0c89 --- /dev/null +++ b/src/core/Akka.Tests/Event/Bugfix5717Specs.cs @@ -0,0 +1,66 @@ +// //----------------------------------------------------------------------- +// // +// // Copyright (C) 2009-2022 Lightbend Inc. +// // Copyright (C) 2013-2022 .NET Foundation +// // +// //----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Akka.Configuration; +using Akka.TestKit; +using Xunit; +using Xunit.Abstractions; + +namespace Akka.Tests.Event +{ + public class Bugfix5717Specs : AkkaSpec + { + public Bugfix5717Specs(ITestOutputHelper output) : base(Config.Empty, output){} + + /// + /// Reproduction for https://github.com/akkadotnet/akka.net/issues/5717 + /// + [Fact] + public async Task Should_unsubscribe_from_all_topics_on_Terminate() + { + var es = Sys.EventStream; + var tm1 = 1; + var tm2 = "FOO"; + var a1 = CreateTestProbe(); + var a2 = CreateTestProbe(); + + es.Subscribe(a1.Ref, typeof(int)); + es.Subscribe(a2.Ref, typeof(int)); + es.Subscribe(a2.Ref, typeof(string)); + es.Publish(tm1); + es.Publish(tm2); + a1.ExpectMsg(tm1); + a2.ExpectMsg(tm1); + a2.ExpectMsg(tm2); + + // kill second test probe + Watch(a2); + Sys.Stop(a2); + ExpectTerminated(a2); + + /* + * It's possible that the `Terminate` message may not have been processed by the + * Unsubscriber yet, so we want to try this operation more than once to see if it + * eventually executes the unsubscribe on the EventStream. + * + * If it still fails after multiple attempts, the issue is that the unsub was never + * executed in the first place. + */ + await AwaitAssertAsync(async () => + { + await EventFilter.DeadLetter().ExpectAsync(0, () => + { + es.Publish(tm1); + es.Publish(tm2); + a1.ExpectMsg(tm1); + }); + }, interval:TimeSpan.FromSeconds(250)); + } + } +} \ No newline at end of file diff --git a/src/core/Akka/Actor/Setup/ActorSystemSetup.cs b/src/core/Akka/Actor/Setup/ActorSystemSetup.cs index dfffbc4e55c..b55e7979199 100644 --- a/src/core/Akka/Actor/Setup/ActorSystemSetup.cs +++ b/src/core/Akka/Actor/Setup/ActorSystemSetup.cs @@ -72,7 +72,8 @@ public Option Get() where T:Setup /// A new, immutable instance. public ActorSystemSetup WithSetup(T setup) where T : Setup { - return new ActorSystemSetup(_setups.SetItem(typeof(T), setup)); + var typeT = setup.GetType(); + return new ActorSystemSetup(_setups.SetItem(typeT, setup)); } /// diff --git a/src/core/Akka/Event/EventBusUnsubscriber.cs b/src/core/Akka/Event/EventBusUnsubscriber.cs index 3bd9b3fd7e5..bf865c879bd 100644 --- a/src/core/Akka/Event/EventBusUnsubscriber.cs +++ b/src/core/Akka/Event/EventBusUnsubscriber.cs @@ -6,10 +6,7 @@ //----------------------------------------------------------------------- using Akka.Actor; -using Akka.Actor.Internal; using Akka.Annotations; -using Akka.Dispatch; -using Akka.Util.Internal; namespace Akka.Event { @@ -26,7 +23,7 @@ namespace Akka.Event /// watching a few actors too much - we opt for the 2nd choice here. /// [InternalApi] - class EventStreamUnsubscriber : ActorBase + internal class EventStreamUnsubscriber : ActorBase { private readonly EventStream _eventStream; private readonly bool _debug; @@ -45,140 +42,83 @@ public EventStreamUnsubscriber(EventStream eventStream, ActorSystem system, bool _debug = debug; } - - /// - /// TBD - /// - /// TBD - /// TBD + protected override bool Receive(object message) { - return message.Match().With(register => + switch (message) { - if (_debug) - _eventStream.Publish(new Debug(this.GetType().Name, GetType(), - string.Format("watching {0} in order to unsubscribe from EventStream when it terminates", register.Actor))); - Context.Watch(register.Actor); - }).With(unregister => - { - if (_debug) - _eventStream.Publish(new Debug(this.GetType().Name, GetType(), - string.Format("unwatching {0} since has no subscriptions", unregister.Actor))); - Context.Unwatch(unregister.Actor); - }).With(terminated => - { - if (_debug) - _eventStream.Publish(new Debug(this.GetType().Name, GetType(), - string.Format("unsubscribe {0} from {1}, because it was terminated", terminated.Actor , _eventStream ))); - _eventStream.Unsubscribe(terminated.Actor); - }) - .WasHandled; + case Register register: + { + if (_debug) + _eventStream.Publish(new Debug(GetType().Name, GetType(), + $"watching {register.Actor} in order to unsubscribe from EventStream when it terminates")); + Context.Watch(register.Actor); + break; + } + case UnregisterIfNoMoreSubscribedChannels unregister: + { + if (_debug) + _eventStream.Publish(new Debug(GetType().Name, GetType(), + $"unwatching {unregister.Actor} since has no subscriptions")); + Context.Unwatch(unregister.Actor); + break; + } + case Terminated terminated: + { + if (_debug) + _eventStream.Publish(new Debug(GetType().Name, GetType(), + $"unsubscribe {terminated.ActorRef} from {_eventStream}, because it was terminated")); + _eventStream.Unsubscribe(terminated.ActorRef); + break; + } + default: + return false; + } + + return true; } - /// - /// TBD - /// protected override void PreStart() { if (_debug) - _eventStream.Publish(new Debug(this.GetType().Name, GetType(), + _eventStream.Publish(new Debug(GetType().Name, GetType(), string.Format("registering unsubscriber with {0}", _eventStream))); - _eventStream.InitUnsubscriber(Self, _system); } /// - /// TBD + /// INTERNAL API + /// + /// Registers a new subscriber to be death-watched and automatically unsubscribed. /// internal class Register { - /// - /// TBD - /// - /// TBD public Register(IActorRef actor) { Actor = actor; } /// - /// TBD - /// - public IActorRef Actor { get; private set; } - } - - - /// - /// TBD - /// - internal class Terminated - { - /// - /// TBD - /// - /// TBD - public Terminated(IActorRef actor) - { - Actor = actor; - } - - /// - /// TBD + /// The actor we're going to deathwatch and automatically unsubscribe /// public IActorRef Actor { get; private set; } } /// - /// TBD + /// INTERNAL API + /// + /// Unsubscribes an actor that is no longer subscribed and does not need to be death-watched any longer. /// internal class UnregisterIfNoMoreSubscribedChannels { - /// - /// TBD - /// - /// TBD public UnregisterIfNoMoreSubscribedChannels(IActorRef actor) { Actor = actor; } /// - /// TBD + /// The actor we're no longer going to death watch. /// public IActorRef Actor { get; private set; } } } - - - - /// - /// Provides factory for Akka.Event.EventStreamUnsubscriber actors with unique names. - /// This is needed if someone spins up more EventStreams using the same ActorSystem, - /// each stream gets it's own unsubscriber. - /// - class EventStreamUnsubscribersProvider - { - private readonly AtomicCounter _unsubscribersCounter = new AtomicCounter(0); - private static readonly EventStreamUnsubscribersProvider _instance = new EventStreamUnsubscribersProvider(); - - - /// - /// TBD - /// - public static EventStreamUnsubscribersProvider Instance - { - get { return _instance; } - } - - /// - /// TBD - /// - /// TBD - /// TBD - /// TBD - public void Start(ActorSystemImpl system, EventStream eventStream, bool debug) - { - system.SystemActorOf(Props.Create(eventStream, system, debug).WithDispatcher(Dispatchers.InternalDispatcherId), - string.Format("EventStreamUnsubscriber-{0}", _unsubscribersCounter.IncrementAndGet())); - } - } } diff --git a/src/core/Akka/Event/EventStream.cs b/src/core/Akka/Event/EventStream.cs index 13c37d21224..0c35364f12b 100644 --- a/src/core/Akka/Event/EventStream.cs +++ b/src/core/Akka/Event/EventStream.cs @@ -11,6 +11,7 @@ using System.Linq; using Akka.Actor; using Akka.Actor.Internal; +using Akka.Dispatch; using Akka.Util; using Akka.Util.Internal; @@ -29,8 +30,13 @@ public class EventStream : LoggingBus { private readonly bool _debug; - private readonly AtomicReference, IActorRef>> _initiallySubscribedOrUnsubscriber = - new AtomicReference, IActorRef>>(); + // used to uniquely name unsubscribers instances, should there be more than one ActorSystem / EventStream + private static readonly AtomicCounter UnsubscribersCounter = new AtomicCounter(0); + private readonly AtomicReference _unsubscriber = new AtomicReference(ActorRefs.NoSender); + + // in the event that an actor subscribers to the EventStream prior to ActorSystemImpl.Init is called + // we register them here and then move them all + private readonly ConcurrentSet _pendingUnsubscribers = new ConcurrentSet(); /// /// Initializes a new instance of the class. @@ -108,87 +114,69 @@ public override bool Unsubscribe(IActorRef subscriber) } /// - /// TBD + /// Used to start the Unsubscriber actor, responsible for garabage-collecting + /// all expired subscriptions when the subscribed actor terminates. /// /// TBD public void StartUnsubscriber(ActorSystemImpl system) { - EventStreamUnsubscribersProvider.Instance.Start(system, this, _debug); - } - - /// - /// TBD - /// - /// TBD - /// TBD - /// TBD - public bool InitUnsubscriber(IActorRef unsubscriber, ActorSystem system) - { - if (system == null) - { - return false; - } - return _initiallySubscribedOrUnsubscriber.Match().With>>(v => + if (_unsubscriber.Value.IsNobody()) { - if (_initiallySubscribedOrUnsubscriber.CompareAndSet(v, Either.Right(unsubscriber))) + lock (this) { - if (_debug) - { - Publish(new Debug(SimpleName(this), GetType(), - string.Format("initialized unsubscriber to: {0} registering {1} initial subscribers with it", unsubscriber, v.Value.Count))); + // not started + var currentValue = _unsubscriber.Value; + var unsubscriber= system.SystemActorOf(Props.Create(this, system, _debug).WithDispatcher(Dispatchers.InternalDispatcherId), + $"EventStreamUnsubscriber-{UnsubscribersCounter.IncrementAndGet()}"); + if (_unsubscriber.CompareAndSet(currentValue, unsubscriber)) + { + // backfill all pending unsubscribers + foreach (var s in _pendingUnsubscribers) + { + unsubscriber.Tell(new EventStreamUnsubscriber.Register(s)); + } + _pendingUnsubscribers.Clear(); + } + else + { + // somehow, despite being locked, we managed to lose the compare and swap + if (_unsubscriber.Value.IsNobody()) + throw new IllegalActorStateException("EventStream is corrupted"); } - v.Value.ForEach(RegisterWithUnsubscriber); - - - } - else - { - InitUnsubscriber(unsubscriber, system); } + } + } - - }).With>(presentUnsubscriber => - { - if (_debug) - { - Publish(new Debug(SimpleName(this), GetType(), - string.Format("not using unsubscriber {0}, because already initialized with {1}", unsubscriber, presentUnsubscriber))); - - } - }).WasHandled; + [Obsolete("Should be removed in 1.5")] + public bool InitUnsubscriber(IActorRef unsubscriber, ActorSystem system) + { + StartUnsubscriber((ActorSystemImpl)system); + return true; } private void RegisterWithUnsubscriber(IActorRef subscriber) { - _initiallySubscribedOrUnsubscriber.Match().With>>(v => + if (_unsubscriber.Value.IsNobody()) { - if (_initiallySubscribedOrUnsubscriber.CompareAndSet(v, - Either.Left>(v.Value.Add(subscriber)))) - { - RegisterWithUnsubscriber(subscriber); - } - - }).With>(unsubscriber => + // pending + _pendingUnsubscribers.TryAdd(subscriber); + } + else { - unsubscriber.Value.Tell( new EventStreamUnsubscriber.Register(subscriber)); - }); + _unsubscriber.Value.Tell( new EventStreamUnsubscriber.Register(subscriber)); + } + } private void UnregisterIfNoMoreSubscribedChannels(IActorRef subscriber) { - _initiallySubscribedOrUnsubscriber.Match().With>>(v => + // not an important operation. If we fail to process this message due to a race condition, then the + // death watch subscription is a no-op anyway. + if (!_unsubscriber.Value.IsNobody()) { - if (_initiallySubscribedOrUnsubscriber.CompareAndSet(v, - Either.Left>(v.Value.Remove(subscriber)))) - { - UnregisterIfNoMoreSubscribedChannels(subscriber); - } - - }).With>(unsubscriber => - { - unsubscriber.Value.Tell(new EventStreamUnsubscriber.UnregisterIfNoMoreSubscribedChannels(subscriber)); - }); + _unsubscriber.Value.Tell(new EventStreamUnsubscriber.UnregisterIfNoMoreSubscribedChannels(subscriber)); + } } } }