You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/09/06 17:32:11 UTC
(juneau) branch jb/j0522a-populate-primitive created (now 8f4c83292a)
This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a change to branch jb/j0522a-populate-primitive
in repository https://gitbox.apache.org/repos/asf/juneau.git
at 8f4c83292a WORK-J0522a: Add the region populate primitive, client-side populator registry, page-scoped message bus and initial-broadcast barrier (juneau-regions.js, PopulatorAllowlist, RegionDef, ViewsMixin serving path), and fold in test-only fix for AsyncResponseProcessor h04 timeout emit race (juneau-integration-tests).
This branch includes the following new commits:
new 8f4c83292a WORK-J0522a: Add the region populate primitive, client-side populator registry, page-scoped message bus and initial-broadcast barrier (juneau-regions.js, PopulatorAllowlist, RegionDef, ViewsMixin serving path), and fold in test-only fix for AsyncResponseProcessor h04 timeout emit race (juneau-integration-tests).
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(juneau) 01/01: WORK-J0522a: Add the region populate primitive, client-side populator registry, page-scoped message bus and initial-broadcast barrier (juneau-regions.js, PopulatorAllowlist, RegionDef, ViewsMixin serving path), and fold in test-only fix for AsyncResponseProcessor h04 timeout emit race (juneau-integration-tests).
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch jb/j0522a-populate-primitive
in repository https://gitbox.apache.org/repos/asf/juneau.git
commit 8f4c83292a811e6c536b4389b9b26470fe9e8ed8
Author: James Bognar <ja...@apache.org>
AuthorDate: Sun Sep 6 13:32:02 2026 -0400
WORK-J0522a: Add the region populate primitive, client-side populator registry, page-scoped message bus and initial-broadcast barrier (juneau-regions.js, PopulatorAllowlist, RegionDef, ViewsMixin serving path), and fold in test-only fix for AsyncResponseProcessor h04 timeout emit race (juneau-integration-tests).
Two unrelated pieces ride in this commit.
1. WORK-J0522a, the foundational child of the unified content-rendering design. A region is a
container the framework hands to a named populate(ctx, container) function; this lands the
primitive and the two coordination mechanisms it needs, and deliberately nothing else - the
declarative default populator, the ctx.helpers bundle and the real host enrolment walks are later
children.
- juneau-regions.js: the populator registry ('default' reserved and frozen at load, an unknown
name failing visibly rather than blank because a region has no raw value to fall back to), the
ctx contract, the page-scoped message bus, and the initial-broadcast barrier.
- The bus broadcasts by default with opt-in targeted delivery, treats payloads as opaque (never
read, cloned or routed on), isolates a throwing subscriber without dropping it, delivers
synchronously in subscription order, and queues rather than nests an emit made during a
delivery. Its cycle cap counts emissions per drain rather than the queue's instantaneous
length, because a two-region ping-pong holds that length at one entry forever.
- The barrier releases when every initial populate settles or at a clamped 2000ms deadline, so
none of the three non-settlement modes - throw, rejection, never settling - can freeze the
page. Its arming protocol is declared by each host runtime rather than inferred from the
clock, with a loud backstop for a runtime that never reports. A released region gets a bounded
replay window whose cap is independent of the bus cycle cap: conflating the two reports a cycle
for legitimate non-cyclic traffic.
- SD-2's emit-ownership invariant, in both directions: chrome auto-emits because the framework
owns it, and content does not, asserted as a negative so a later convenience click delegate
over author DOM fails a test instead of silently breaking the rule.
- Server side: PopulatorAllowlist (an authoring-discipline control, not a security boundary) and
a deliberately minimal RegionDef with its own CONTRACT_VERSION, plus ViewsMixin serving the new
asset.
- Tests: 48 always-on Java tests over three Node harnesses that run the real runtime source under
a DOM shim and a controllable clock, several asserting against that clock rather than against
delivery order because delivery is correct under both readings and only the latency differs.
2. A test-only fix for the AsyncResponseProcessor h04 timeout-emit race in juneau-integration-tests.
finalizeAsync emits the completion-path record AFTER flushBuffer by design, and on the timeout
path the emitting thread is the container's own async-timeout dispatch thread, so a blocking HTTP
client can read the flushed body before the emit lands. h04 now polls for the record under a
bounded deadline instead of reading it immediately; the 'exactly one record' assertion is
unchanged, and there is no production change.
---
...ncResponseProcessor_JettyMicroservice_Test.java | 35 +-
.../rest/server/views/PopulatorAllowlist.java | 69 +
.../apache/juneau/rest/server/views/RegionDef.java | 135 ++
.../juneau/rest/server/views/ViewsMixin.java | 46 +-
.../org/apache/juneau/views/juneau-regions.js | 1395 ++++++++++++++++++++
.../rest/server/views/PopulatorAllowlist_Test.java | 108 ++
.../juneau/rest/server/views/RegionDef_Test.java | 75 ++
.../juneau/rest/server/views/RegionsHarness.java | 156 +++
.../rest/server/views/Regions_Barrier_Test.java | 343 +++++
.../juneau/rest/server/views/Regions_Bus_Test.java | 283 ++++
.../rest/server/views/Regions_Primitive_Test.java | 334 +++++
.../rest/server/views/ViewsMixin_Serving_Test.java | 13 +-
.../src/test/js/regions-barrier.cjs | 736 +++++++++++
.../src/test/js/regions-bus.cjs | 429 ++++++
.../src/test/js/regions-harness.cjs | 251 ++++
.../src/test/js/regions-primitive.cjs | 595 +++++++++
16 files changed, 4993 insertions(+), 10 deletions(-)
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/processor/AsyncResponseProcessor_JettyMicroservice_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/processor/AsyncResponseProcessor_JettyMicroservice_Test.java
index 2ffd775891..6950447e5a 100644
--- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/processor/AsyncResponseProcessor_JettyMicroservice_Test.java
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/processor/AsyncResponseProcessor_JettyMicroservice_Test.java
@@ -420,6 +420,37 @@ class AsyncResponseProcessor_JettyMicroservice_Test extends TestBase {
return HTTP.send(req, BodyHandlers.ofString());
}
+ /**
+ * Polls {@code handler.forLogger(loggerName)} until at least {@code minCount} records have been captured, or a
+ * 5-second deadline elapses.
+ *
+ * <p>
+ * {@code AsyncResponseProcessor.finalizeAsync(...)} emits the completion-path record AFTER {@code res.flushBuffer()}
+ * has already written the response to the client (by design — see that method's javadoc: "Completion-path emission
+ * runs AFTER the body/headers are written"). A test that calls a blocking HTTP client and then immediately inspects
+ * the collecting handler is racing the server-side emission against the client's own wake-up from the socket read.
+ * That race is normally unobservable (the emit is the very next statement on the same thread that just flushed), but
+ * widens under load on the async-timeout path specifically: there, the emitting thread is the container's own
+ * async-timeout dispatch thread (invoked via {@code AsyncListener.onTimeout}) rather than a thread whose only
+ * remaining job is to emit-then-return, so it can be preempted between the flush and the emit. Bounded polling
+ * (instead of loosening the assertion) keeps the "exactly one record" contract intact while tolerating that
+ * scheduling gap; the fast, overwhelmingly common case (record already present) pays no extra latency.
+ *
+ * @param handler The collecting handler to poll.
+ * @param loggerName The logger name to filter on.
+ * @param minCount The minimum record count to wait for.
+ * @return The current record list for {@code loggerName} (may have fewer than {@code minCount} entries if the
+ * deadline elapsed).
+ * @throws InterruptedException If interrupted while waiting.
+ */
+ private static List<LogRecord> awaitRecords(CollectingHandler handler, String loggerName, int minCount) throws InterruptedException {
+ var deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
+ List<LogRecord> recs;
+ while ((recs = handler.forLogger(loggerName)).size() < minCount && System.nanoTime() < deadline)
+ Thread.sleep(10);
+ return recs;
+ }
+
@Test void h01_delayedAsync_emitsOnCompletionPath_notOnFinish() throws Exception {
var opLogger = Logger.getLogger(AsyncCaptureServlet.class.getName() + ".delayed");
var state = new LoggerState(opLogger);
@@ -524,7 +555,9 @@ class AsyncResponseProcessor_JettyMicroservice_Test extends TestBase {
var resp = capGet("/asyncCap/neverCompletes");
assertEquals(504, resp.statusCode(), resp.body());
- var recs = handler.forLogger(opLogger.getName());
+ // The timeout path emits on the container's async-timeout dispatch thread, AFTER the 504 has already been
+ // flushed to this client — poll instead of asserting immediately (see awaitRecords javadoc).
+ var recs = awaitRecords(handler, opLogger.getName(), 1);
assertEquals(1, recs.size(), "timeout path must still emit exactly one completion-path record");
var rec = recs.get(0);
assertEquals(Level.INFO, rec.getLevel());
diff --git a/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/PopulatorAllowlist.java b/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/PopulatorAllowlist.java
new file mode 100644
index 0000000000..eec98d38b0
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/PopulatorAllowlist.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.views;
+
+import static org.apache.juneau.commons.utils.Shorts.*;
+
+import java.util.*;
+
+/**
+ * Serving-path allowlist of populator names that may be named on a region ({@link RegionDef#populate}).
+ *
+ * <p>
+ * This is an <b>authoring-discipline control, not a security boundary</b>, and the reason is structural, not a
+ * weaker policy choice. {@link SinkRenderAllowlist} is real defense: a named renderer's markup still has to pass
+ * through the closed {@code fillRenderSlot} copier, so an accepted id only ever gets to write what that copier lets
+ * it write. A populator has no such copier anywhere in its path — {@code JuneauViews.regions.resolve(name)}
+ * hands the resolved function the region's container element outright, and from that point the populator owns the
+ * container completely. This class therefore controls only <i>which name is resolved</i>; it says nothing whatever
+ * about what the resolved populator then does with the container it is handed. A reader who transfers
+ * {@link RowDetailDef#allowCustomRenderers}'s security value onto this allowlist will over-trust it.
+ *
+ * @since 10.0.0
+ */
+public final class PopulatorAllowlist {
+
+ /**
+ * The reserved declarative-default populator name — the only Juneau-shipped populator today.
+ *
+ * <p>
+ * Unlike an ordinary {@code JuneauViews.regions.register(name, fn)} entry, which is last-write-wins and freely
+ * overridable by a later registration under the same name, the client registry refuses to let a consumer
+ * override the entry named <js>"default"</js>: that name is reserved for the framework's own declarative-default
+ * populator.
+ */
+ public static final Set<String> BUILTIN_IDS = Set.of("default");
+
+ private PopulatorAllowlist() {}
+
+ /**
+ * Accepts a reserved built-in name, or a name present in {@code allowedNames}.
+ *
+ * @param populatorName The populator name. Must not be <jk>null</jk> or blank.
+ * @param allowedNames Opt-in populator names (e.g. {@link RegionDef#allowedPopulators}). Can be <jk>null</jk>.
+ * @throws IllegalArgumentException If the name is neither a built-in nor an opted-in name.
+ */
+ public static void assertAllowed(String populatorName, Collection<String> allowedNames) {
+ if (populatorName == null || populatorName.isBlank())
+ throw iaex("Populator name must not be null or blank.");
+ if (BUILTIN_IDS.contains(populatorName))
+ return;
+ if (allowedNames != null && allowedNames.contains(populatorName))
+ return;
+ throw iaex("Populator name '%s' is not an allowed region populator.", populatorName);
+ }
+}
diff --git a/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/RegionDef.java b/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/RegionDef.java
new file mode 100644
index 0000000000..b121da84c1
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/RegionDef.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.views;
+
+import static org.apache.juneau.commons.utils.Shorts.*;
+
+import java.util.*;
+
+/**
+ * A page "region": a named container whose contents a client-side populator fills in, chosen by name via
+ * {@link #populate} rather than declared inline.
+ *
+ * <p>
+ * This type is deliberately minimal. A region's full descriptor shape — its data source, request parameters,
+ * an inline renderer, lazy activation, a refresh interval, and a field/title projection — together with the
+ * sidecar it projects into, is a separate, larger contract that a later revision of this type owns. This first
+ * pass carries only what is needed to name a region, select a populator for it by name, and validate that selection
+ * against the server-side allowlist. Do not add data-source, refresh, or field-projection fields here; they belong
+ * to that larger contract, not to this one.
+ *
+ * @since 10.0.0
+ */
+@SuppressWarnings({
+ "java:S1845" // Fluent-builder setters intentionally mirror field names (Juneau DSL convention).
+})
+public class RegionDef {
+
+ /**
+ * The frozen contract version for the region envelope.
+ *
+ * <p>
+ * Deliberately independent of {@link ViewDef#CONTRACT_VERSION} and {@link RowDetailDef#CONTRACT_VERSION}, for
+ * the same reason those two envelopes are independent of each other: a region-contract revision must never
+ * force a view- or detail-sidecar bump, or vice-versa.
+ */
+ public static final String CONTRACT_VERSION = "1";
+
+ /** This region's own id, unique within its host. */
+ public String id;
+
+ /**
+ * The name of the client populator to resolve via {@code JuneauViews.regions.resolve(name)}, or <jk>null</jk>
+ * for the reserved declarative default (<js>"default"</js>, see {@link PopulatorAllowlist#BUILTIN_IDS}).
+ *
+ * <p>
+ * This is a name looked up in the client registry rather than an inline function for two reasons: an inline
+ * function cannot survive the JSON sidecar this envelope travels in, and a name — unlike an arbitrary
+ * function body — is the one shape a server-side allowlist ({@link PopulatorAllowlist}) can meaningfully
+ * constrain at all.
+ */
+ public String populate;
+
+ /**
+ * App-approved populator names allowed on {@link #populate} in addition to
+ * {@link PopulatorAllowlist#BUILTIN_IDS}. Blank entries fail {@link #validate()}.
+ *
+ * <p>
+ * Opting a name in here is authoring discipline, not a security boundary — see
+ * {@link PopulatorAllowlist}'s class Javadoc.
+ */
+ public Set<String> allowedPopulators;
+
+ /**
+ * Creates a region with the given id.
+ *
+ * @param id This region's own id, unique within its host. Must not be <jk>null</jk> or blank.
+ * @return A new {@link RegionDef}.
+ */
+ public static RegionDef create(String id) {
+ var r = new RegionDef();
+ r.id = id;
+ return r;
+ }
+
+ /**
+ * Sets the name of the client populator to resolve.
+ *
+ * @param value A populator name, or <jk>null</jk> to use the reserved declarative default.
+ * @return This object.
+ */
+ public RegionDef populate(String value) {
+ populate = value;
+ return this;
+ }
+
+ /**
+ * Opts in app-approved populator names for {@link #populate}, in addition to
+ * {@link PopulatorAllowlist#BUILTIN_IDS}.
+ *
+ * @param value Populator names. Must not contain blank entries.
+ * @return This object.
+ */
+ public RegionDef allowPopulators(String...value) {
+ allowedPopulators = st(value);
+ return this;
+ }
+
+ /**
+ * Fail-closed bean validation: rejects a missing/blank {@link #id}, a blank {@link #allowedPopulators} entry,
+ * and (when {@link #populate} is set) a populator name that {@link PopulatorAllowlist} does not allow.
+ *
+ * <p>
+ * The populator-name check happens here, at startup, rather than at paint time. The client runtime fails
+ * closed on a name it cannot resolve, which is the right runtime behaviour and the wrong startup behaviour: a
+ * typo left to surface there silently disables the region on every page that reaches it, with no one watching
+ * for the console warning. So the typo fails loud here instead, at the one point that can see both the
+ * declared name and the allowlist.
+ *
+ * @throws IllegalArgumentException If this definition is not well-formed.
+ */
+ public void validate() {
+ if (id == null || id.isBlank())
+ throw iaex("RegionDef id must not be null or blank.");
+ if (allowedPopulators != null)
+ for (var name : allowedPopulators)
+ if (name == null || name.isBlank())
+ throw iaex("allowPopulators entry must not be blank.");
+ if (populate != null)
+ PopulatorAllowlist.assertAllowed(populate, allowedPopulators);
+ }
+}
diff --git a/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/ViewsMixin.java b/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/ViewsMixin.java
index 34dd6d54ae..a05cccca2e 100644
--- a/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/ViewsMixin.java
+++ b/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/ViewsMixin.java
@@ -24,9 +24,10 @@ import org.apache.juneau.rest.server.widgets.*;
/**
* Mixin that serves the first-party rich-view runtime assets — {@code juneau-views.js},
* {@code juneau-ribbon.js}, {@code juneau-renders.js}, {@code juneau-views.css}, the opt-in
- * {@code juneau-pages.js} tabs/sub-tabs page runtime, the opt-in {@code juneau-config.js}/
- * {@code juneau-config.css} column-chooser runtime — each at its stable path (design doc §6.1), plus
- * deprecated compatibility mounts for the four assets that have since moved to the widget module.
+ * {@code juneau-pages.js} tabs/sub-tabs page runtime, the opt-in {@code juneau-regions.js} region-populate runtime,
+ * the opt-in {@code juneau-config.js}/{@code juneau-config.css} column-chooser runtime — each at its stable
+ * path (design doc §6.1), plus deprecated compatibility mounts for the four assets that have since moved to the
+ * widget module.
*
* <p>
* Compose into a host resource via {@link Rest#mixins() @Rest(mixins=ViewsMixin.class)}; the asset URLs then become
@@ -127,6 +128,18 @@ public class ViewsMixin {
*/
public static final String PAGES_JS_PATH = "/juneau-pages.js";
+ /**
+ * The URL path at which the opt-in region-populate runtime is served (relative to the host mount). A page with
+ * no {@code data-juneau-region} element never needs to load it.
+ *
+ * <h5 class='section'>Load order is a contract, not a preference:</h5>
+ * <p>
+ * This {@code <script>} MUST come after {@code juneau-views.js}. The region runtime publishes onto the same
+ * {@code window.JuneauViews} namespace that {@code juneau-views.js} creates, and reuses that runtime's
+ * {@code renderAsyncStatus} status renderer rather than defining one of its own.
+ */
+ public static final String REGIONS_JS_PATH = "/juneau-regions.js";
+
/**
* The URL path at which the opt-in column-chooser runtime is served (relative to the host mount). A
* consumer adds this {@code <script>} after {@code juneau-views.js}; a non-configurable table never loads it.
@@ -253,6 +266,9 @@ public class ViewsMixin {
/** Classpath location of the shipped page runtime. */
static final String PAGES_JS_RESOURCE = "/org/apache/juneau/views/juneau-pages.js";
+ /** Classpath location of the shipped region-populate runtime. */
+ static final String REGIONS_JS_RESOURCE = "/org/apache/juneau/views/juneau-regions.js";
+
/** Classpath location of the shipped column-chooser runtime. */
static final String CONFIG_JS_RESOURCE = "/org/apache/juneau/views/juneau-config.js";
@@ -417,6 +433,21 @@ public class ViewsMixin {
return serve(PAGES_JS_RESOURCE, JS_CONTENT_TYPE);
}
+ /**
+ * [GET /juneau-regions.js] — serve the opt-in region-populate runtime.
+ *
+ * @return The region-populate runtime as a JavaScript {@link HttpResource}.
+ */
+ @RestGet(
+ path=REGIONS_JS_PATH,
+ summary="Juneau rich-view region-populate runtime",
+ description="First-party, opt-in JavaScript that resolves a name-keyed populator against a data-juneau-region container.",
+ swagger=@OpSwagger(ignore=true)
+ )
+ public HttpResource getRegionsScript() {
+ return serve(REGIONS_JS_RESOURCE, JS_CONTENT_TYPE);
+ }
+
/**
* [GET /juneau-config.js] — serve the opt-in column-chooser / saved-views runtime.
*
@@ -475,8 +506,8 @@ public class ViewsMixin {
*
* @param path One of the asset path constants ({@link #VIEWS_JS_PATH}, {@link #RIBBON_JS_PATH},
* {@link #RENDERS_JS_PATH}, {@link #VIEWS_CSS_PATH}, {@link #ICONS_JS_PATH}, {@link #SYMBOLS_SVG_PATH},
- * {@link #PAGES_JS_PATH}, {@link #CONFIG_JS_PATH}, {@link #CONFIG_CSS_PATH}, {@link #CARDS_JS_PATH},
- * {@link #CALENDAR_JS_PATH}, {@link #CALENDAR_CSS_PATH}).
+ * {@link #PAGES_JS_PATH}, {@link #REGIONS_JS_PATH}, {@link #CONFIG_JS_PATH}, {@link #CONFIG_CSS_PATH},
+ * {@link #CARDS_JS_PATH}, {@link #CALENDAR_JS_PATH}, {@link #CALENDAR_CSS_PATH}).
* @return The servlet-relative asset URL with the version+content-hash cache-buster appended.
*/
public static String viewAssetUrl(String path) {
@@ -499,8 +530,8 @@ public class ViewsMixin {
* @param req The current request, supplying the context path/mount to resolve against.
* @param path One of the asset path constants ({@link #VIEWS_JS_PATH}, {@link #RIBBON_JS_PATH},
* {@link #RENDERS_JS_PATH}, {@link #VIEWS_CSS_PATH}, {@link #ICONS_JS_PATH}, {@link #SYMBOLS_SVG_PATH},
- * {@link #PAGES_JS_PATH}, {@link #CONFIG_JS_PATH}, {@link #CONFIG_CSS_PATH}, {@link #CARDS_JS_PATH},
- * {@link #CALENDAR_JS_PATH}, {@link #CALENDAR_CSS_PATH}).
+ * {@link #PAGES_JS_PATH}, {@link #REGIONS_JS_PATH}, {@link #CONFIG_JS_PATH}, {@link #CONFIG_CSS_PATH},
+ * {@link #CARDS_JS_PATH}, {@link #CALENDAR_JS_PATH}, {@link #CALENDAR_CSS_PATH}).
* @return The absolute asset URL with the version+content-hash cache-buster appended.
*/
public static String viewAssetUrl(RestRequest req, String path) {
@@ -594,6 +625,7 @@ public class ViewsMixin {
if (ICONS_JS_PATH.equals(path)) return ICONS_JS_RESOURCE;
if (SYMBOLS_SVG_PATH.equals(path)) return SYMBOLS_SVG_RESOURCE;
if (PAGES_JS_PATH.equals(path)) return PAGES_JS_RESOURCE;
+ if (REGIONS_JS_PATH.equals(path)) return REGIONS_JS_RESOURCE;
if (CONFIG_JS_PATH.equals(path)) return CONFIG_JS_RESOURCE;
if (CONFIG_CSS_PATH.equals(path)) return CONFIG_CSS_RESOURCE;
if (CARDS_JS_PATH.equals(path)) return CARDS_JS_RESOURCE;
diff --git a/juneau-rest/juneau-rest-server-views/src/main/resources/org/apache/juneau/views/juneau-regions.js b/juneau-rest/juneau-rest-server-views/src/main/resources/org/apache/juneau/views/juneau-regions.js
new file mode 100644
index 0000000000..7b1792a199
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-views/src/main/resources/org/apache/juneau/views/juneau-regions.js
@@ -0,0 +1,1395 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * juneau-regions.js - the region runtime: one `populate(ctx, container)` seam, a name-keyed populator registry, and
+ * one page-scoped message bus.
+ *
+ * A REGION is an empty, framework-owned container whose body is drawn by a named JavaScript function instead of by
+ * server-emitted markup. The framework owns the container, its identity, its lifecycle and its cancellation; the
+ * populator owns everything inside it. Three region types exist and no more: "row-detail", "card-body", "tab-body".
+ *
+ * JuneauViews.regions.register("my-populator", function populate(ctx, container) {
+ * // container is empty, in-document, and mine.
+ * // Return nothing, a cleanup function, a Promise, or a Promise of a cleanup function.
+ * });
+ *
+ * LOAD ORDER IS A CONTRACT, NOT A PREFERENCE. This script MUST come after juneau-views.js: it publishes onto the
+ * window.JuneauViews namespace that file creates, and it reuses that file's renderAsyncStatus status renderer and its
+ * same-origin URL / CSRF / safe-method guards rather than defining second copies of them. A page with no
+ * [data-juneau-region] element never needs to load it, and a page that loads it but has no regions pays nothing: the
+ * bus and the barrier are both created at first enrolment, never by a document scan.
+ *
+ * NO EMITTER IS WIRED HERE. This file is the seam and the channel; the chrome call sites that enrol regions and the
+ * chrome events that auto-emit onto the bus are a separate, later edit. Nothing that ships today behaves
+ * differently because this asset exists, which is the property that lets the riskiest construction in the design land
+ * before anything depends on it.
+ *
+ * PLATFORM BASELINE. AbortController/AbortSignal are a hard requirement, feature-detected exactly once at load and
+ * never branched on again. Every shipped Juneau asset already uses optional chaining, which is ES2020 syntax and
+ * therefore a PARSE-time dependency - a browser that cannot parse juneau-views.js loads no region runtime at all, and
+ * AbortController predates optional chaining across every engine. So the baseline adds no requirement a consumer did
+ * not already have; it only writes down one that was already true. When the global is genuinely absent the runtime
+ * REFUSES to populate and says so, rather than degrading to a signal-less ctx: ctx.signal is load-bearing in three
+ * separate contracts (teardown abort, fork()'s supersede-the-previous-request mechanism, and ctx.write's prior-write
+ * abort) and none of them has a non-signal fallback.
+ */
+(function () {
+ "use strict";
+
+ const NS = window.JuneauViews = window.JuneauViews || {};
+
+ // ==================================================================================================================
+ // CONSTANTS
+ // ==================================================================================================================
+
+ /**
+ * The region contract version, echoed onto every ctx so a consumer that wants to assert it can. Deliberately
+ * independent of the view, action-result, bulk, detail and nested envelope versions - a region-contract revision
+ * must never force one of those to bump, or vice-versa. MUST equal RegionDef.CONTRACT_VERSION on the server.
+ */
+ const REGION_CONTRACT_VERSION = "1";
+
+ /** The region container's identity attributes - MUST equal the server's constants of the same names. */
+ const REGION_ATTR = "data-juneau-region";
+ const REGION_TYPE_ATTR = "data-juneau-region-type";
+ const REGION_HOST_ATTR = "data-juneau-region-host";
+ const REGION_POPULATE_ATTR = "data-juneau-region-populate";
+
+ /**
+ * The enrolment mark, reflected as an attribute so the region's own loading/ok/error state is assertable from a
+ * test and visible in devtools. Cleared by teardown and by nothing else, so a marked-but-disconnected node is a
+ * detectable bug rather than an invisible one.
+ */
+ const REGION_STATE_ATTR = "data-juneau-region-state";
+
+ /** The three region types. Three, not four: a detail body is one region, not a strip of sections. */
+ const REGION_TYPES = ["row-detail", "card-body", "tab-body"];
+
+ /**
+ * The reserved populator name, and the whole of the reserved set. `register("default", ...)` is refused so that
+ * the declarative default's non-privilege argument stays true: the name must resolve to Juneau's default and not
+ * to something a consumer swapped in. Every OTHER Juneau-shipped populator is an ordinary, overridable registry
+ * entry - freezing the whole shipped set would make it a compatibility surface, so that adding a populator called
+ * "chart" in a later release would break a consumer that had already registered "chart".
+ */
+ const DEFAULT_POPULATOR_NAME = "default";
+ const RESERVED_POPULATOR_NAMES = [DEFAULT_POPULATOR_NAME];
+
+ /** Framework-emitted `kind` values are namespaced. A consumer `kind` must not start with this prefix. */
+ const FRAMEWORK_KIND_PREFIX = "juneau:";
+
+ /** `meta.from` for a framework-emitted message: the framework, not a region. */
+ const FRAMEWORK_SENDER_KEY = "juneau:framework";
+
+ /**
+ * The emit QUEUE DEPTH cap: how many emissions ONE DRAIN may queue from inside its own delivery before the drain
+ * is declared cyclic and terminated. It is deliberately never consulted for a top-of-turn emit or for the
+ * initial emissions the barrier holds - a page with 65 control regions each emitting once as it paints contains no
+ * cycle, and reporting one would be a false diagnostic - and it is NOT a message budget over the life of a page or
+ * of a replay buffer.
+ *
+ * It counts emissions-per-drain rather than the queue's INSTANTANEOUS length, and the difference is the whole
+ * reason the cap terminates anything. The obvious reading - cap `queue.length` - catches a cycle that nests,
+ * because nesting grows the queue faster than delivery drains it. It does not catch the simplest cycle there is:
+ * two regions each re-emitting on the other's message hold the queue at exactly one pending entry forever, since
+ * every delivery removes one and adds one, so an instantaneous-length cap is never reached and the drain loops
+ * until the tab is killed. Counting the emissions instead makes both shapes terminate at the same bound.
+ */
+ const BUS_QUEUE_DEPTH_CAP = 64;
+
+ /**
+ * The initial-drain replay buffer's LENGTH cap: appends over one buffer's lifetime. Ordinary live traffic grows
+ * it monotonically and nothing removes an entry until the buffer is discarded whole, so reaching it means a
+ * long-lived released window rather than a defect in the traffic - which is why it is four times the queue-depth
+ * cap and why its overflow is a buffer abandonment and never a cycle report. A queue that reaches depth 64 is
+ * pathological; a buffer that reaches length 64 is a moderately busy page. The two caps are independent in both
+ * directions and must not be collapsed into one constant.
+ */
+ const REPLAY_BUFFER_CAP = 256;
+
+ /**
+ * The page-wide initial-broadcast barrier deadline, a CLAMPED constant and not a consumer-settable one: an
+ * author-settable barrier deadline is a way to configure a page-wide freeze, which is the same reasoning ViewDef
+ * gives for flooring a declarable poll interval. Long enough for a normal first paint including one declared
+ * fetch, short enough that a user does not perceive a control as dead. Because a released region also gets a
+ * bounded replay of the drain it missed, this number is a latency knob rather than a correctness one.
+ */
+ const BARRIER_DEADLINE_MS = 2000;
+
+ /** How many (from -> to) hops are retained for the cycle diagnostic. Bounded; a ring, not a log. */
+ const CYCLE_TRACE_CAP = 256;
+
+ /** The abort reason a fork uses when it supersedes the previous fork or message signal from the same region. */
+ const SUPERSEDED_REASON = "juneau:superseded";
+
+ /** The abort reason a re-populate uses, distinct from teardown so a consumer can tell the two apart. */
+ const REPOPULATE_REASON = "juneau:region-repopulated";
+
+ /** The abort reason teardown uses. */
+ const TEARDOWN_REASON = "juneau:region-torn-down";
+
+ /**
+ * Builds the value an abort is reported WITH, and the shape is load-bearing rather than cosmetic.
+ *
+ * `controller.abort(someString)` makes `signal.reason` that string, and `fetch` rejects with `signal.reason`
+ * verbatim - so a populate running the documented swallow (`catch(e) { if (e.name !== "AbortError") throw e; }`)
+ * would read `e.name` as `undefined` on a string reason and RETHROW the cancellation it meant to ignore, turning
+ * every supersede into a surfaced region error. Aborting with no argument avoids that but then teardown, a
+ * re-populate and a supersede are indistinguishable. A DOMException named "AbortError" gets both: the platform
+ * contract every author and every third-party library already tests for, plus a message saying which of the three
+ * happened.
+ */
+ function abortReason(reason) {
+ if (typeof DOMException === "function") return new DOMException(reason, "AbortError");
+ const e = new Error(reason);
+ e.name = "AbortError";
+ return e;
+ }
+
+ /**
+ * The ONE platform-baseline feature test in this runtime. Evaluated at load and never re-evaluated: a second,
+ * per-call test is how a silent degrade creeps back in.
+ */
+ const HAS_ABORT_CONTROLLER = typeof AbortController !== "undefined";
+
+ // ==================================================================================================================
+ // SHARED STATE (all page-scoped, all created here, none exposed for enumeration)
+ // ==================================================================================================================
+
+ /** name -> populate fn. Last-write-wins, exactly like the renderer registry. */
+ const registry = {};
+
+ /** Juneau-shipped populators, resolved ahead of the consumer registry. The reserved default lands here. */
+ const builtins = {};
+
+ /** Every live region on the page, in enrolment order. The bus resolves a `{to}` against this. */
+ const liveRegions = [];
+
+ /**
+ * The bus. Deliberately NOT exposed as an object a region can enumerate: a region only ever sees its own ctx.emit
+ * and ctx.on, so the framework can sweep a region's subscriptions wholesale at teardown - which is exactly the
+ * capability a global `bus.on(...)` would not have.
+ */
+ const bus = {
+ subscribers: [],
+ queue: [],
+ fanOutDepth: 0,
+ draining: false,
+ queuedThisDrain: 0,
+ hops: []
+ };
+
+ /** The one page-scoped barrier. "unarmed" until the first enrolment; "lifted" exactly once thereafter. */
+ const barrier = {
+ state: "unarmed",
+ outstanding: [],
+ runtimes: [],
+ ready: [],
+ deadlineTimer: null,
+ backstopArmed: false,
+ warned: false
+ };
+
+ /** The initial-drain replay buffer. One per page load; never reconstituted after an overflow. */
+ const replay = {
+ open: false,
+ abandoned: false,
+ buffer: [],
+ windows: []
+ };
+
+ /** Set once when the AbortController baseline refusal has been reported, so a page gets one error and not N. */
+ let baselineReported = false;
+
+ // ==================================================================================================================
+ // SMALL HELPERS
+ // ==================================================================================================================
+
+ function isElement(n) {
+ return !!n && n.nodeType === 1;
+ }
+
+ function isConnected(el) {
+ if (typeof el.isConnected === "boolean") return el.isConnected;
+ return !!window.document?.contains?.(el);
+ }
+
+ function blank(s) {
+ return s == null || String(s).trim() === "";
+ }
+
+ function keysOf(o) {
+ return o ? Object.keys(o) : [];
+ }
+
+ /**
+ * Visibility, for the "is this region's first populate an initial one" question. Mirrors the card runtime's
+ * predicate: the `hidden` attribute, an inline display:none, a non-active tab/subtab panel ancestor, and a
+ * computed display:none.
+ */
+ function isRegionHidden(el) {
+ let n = el;
+ while (isElement(n)) {
+ if (n.hidden) return true;
+ if (n.style?.display === "none") return true;
+ const cls = " " + (n.className || "") + " ";
+ if ((cls.indexOf(" jc-panel ") >= 0 || cls.indexOf(" jc-subpanel ") >= 0) && cls.indexOf(" jc-active ") < 0)
+ return true;
+ const computed = window.getComputedStyle ? window.getComputedStyle(n) : null;
+ if (computed?.display === "none") return true;
+ n = n.parentNode;
+ }
+ return false;
+ }
+
+ /** Clears a container without touching the region mark: R4's clear-before-every-call must not re-mint a region. */
+ function clearContainer(el) {
+ if (typeof el.replaceChildren === "function") { el.replaceChildren(); return; }
+ while (el.firstChild) el.firstChild.remove();
+ }
+
+ /**
+ * Reflects the region's loading/ok/error state.
+ *
+ * The state is carried by an ATTRIBUTE, and the view runtime's owned status paragraph is painted only for the
+ * error state - deliberately, and it is the one place this runtime declines to reuse renderAsyncStatus verbatim.
+ * That function appends a `<p role="status">` INTO the container it is given (juneau-views.js:1406-1410), and a
+ * region's container has to be EMPTY when the populator is handed it: painting a loading paragraph first would
+ * hand every populator a container with a framework child already in it, and painting one afterwards would append
+ * a framework node into DOM the author owns. On the error path neither objection applies, because the container
+ * has just been cleared and its contents are the framework's own message rather than the author's content.
+ */
+ function setRegionState(region, state, message) {
+ region.state = state;
+ region.el.setAttribute(REGION_STATE_ATTR, state);
+ if (state !== "error") return;
+ const render = NS.init?.renderAsyncStatus;
+ if (typeof render === "function") render(region.el, "error", message);
+ }
+
+ // ==================================================================================================================
+ // REGISTRY (modeled on the renderer registry, with two deliberate differences)
+ // ==================================================================================================================
+
+ /**
+ * Registers a populator under `name`. Last-write-wins, exactly like registerRenderer - a consumer may override a
+ * Juneau-shipped populator, with the ONE exception of the reserved default, which is refused with a console error
+ * and no mutation.
+ */
+ function register(name, populateFn) {
+ if (blank(name)) {
+ window.console.error("JuneauViews.regions.register: a populator name must not be blank.");
+ return null;
+ }
+ if (RESERVED_POPULATOR_NAMES.indexOf(name) >= 0) {
+ window.console.error("JuneauViews.regions.register: '" + name
+ + "' is a reserved populator name and cannot be overridden.");
+ return null;
+ }
+ if (typeof populateFn !== "function") {
+ window.console.error("JuneauViews.regions.register: populator '" + name + "' must be a function.");
+ return null;
+ }
+ registry[name] = populateFn;
+ return populateFn;
+ }
+
+ /**
+ * Looks up a populator by name; returns null when unknown. Unlike a renderer, an unknown name FAILS VISIBLY at
+ * the call site rather than falling back - a field has a raw value to fall back to and a region does not.
+ */
+ function resolve(name) {
+ if (typeof name !== "string") return null;
+ if (Object.hasOwn(builtins, name)) return builtins[name];
+ return Object.hasOwn(registry, name) ? registry[name] : null;
+ }
+
+ // ==================================================================================================================
+ // CANCELLATION (per-invocation signal, per-delivery signal, and fork())
+ // ==================================================================================================================
+
+ /**
+ * Attaches `fork` to a genuine platform AbortSignal as a non-enumerable, non-writable OWN property.
+ *
+ * NEVER on AbortSignal.prototype: a page hosting a region and an unrelated library on the same document would
+ * then see the framework's method on THEIR signals. Non-enumerable so that a key-set walk of the ctx or of the
+ * signal does not see it. The signal itself stays a real AbortSignal - `instanceof` holds, `aborted`/`reason`/
+ * `addEventListener`/`throwIfAborted` all behave normally, and it can be handed to fetch or to a third-party
+ * library with no adapter, which is the property that decided this shape over a wrapper object.
+ */
+ function attachFork(region, signal) {
+ Object.defineProperty(signal, "fork", {
+ value: function () { return forkFrom(region); },
+ enumerable: false,
+ writable: false,
+ configurable: false
+ });
+ return signal;
+ }
+
+ /**
+ * Returns a fresh child signal, aborting the previous fork from the same region and incrementing ctx.generation.
+ *
+ * A consumer may hold at most ONE live fork per region and should not try to hold more - forking again cancels
+ * the previous one, and that IS the mechanism. A populate that genuinely needs two concurrent, independently
+ * cancellable requests allocates its own AbortController and returns a cleanup function; that is a legitimate
+ * case and this method does not cover it. The child is a PLAIN AbortSignal with no `fork` of its own: forking is
+ * a capability of the invocation signal, not a chainable operation, so the tree stays exactly two levels deep and
+ * "which fork aborted which" stays answerable.
+ */
+ function forkFrom(region) {
+ if (region.lastFork && !region.lastFork.signal.aborted) region.lastFork.abort(abortReason(SUPERSEDED_REASON));
+ if (region.lastForkUnlink) region.lastForkUnlink();
+ const child = new AbortController();
+ const link = linkToParent(region.signalCtl.signal, child);
+ region.lastFork = child;
+ region.lastForkUnlink = link.unlink;
+ region.generation++;
+ if (region.ctx) region.ctx.generation = region.generation;
+ return link.signal;
+ }
+
+ /**
+ * Wires parent -> child abort with one listener, and returns the CHILD CONTROLLER'S OWN signal.
+ *
+ * Deliberately not AbortSignal.any, even where the platform has it: `any` returns a THIRD signal that is neither
+ * the parent's nor the child's, so the framework would hold a controller whose `signal.aborted` stays false after
+ * the parent aborts while the consumer holds a different object that reports true. Two objects disagreeing about
+ * one cancellation is how "which fork aborted which" stops being answerable, and the supersede check reads the
+ * framework's side. The listener is unlinked when the fork is superseded, so a region that forks on every message
+ * does not accumulate one listener per fork on its invocation signal.
+ */
+ function linkToParent(parent, childCtl) {
+ if (parent.aborted) {
+ childCtl.abort(parent.reason);
+ return { signal: childCtl.signal, unlink: function () { /* nothing was listening */ } };
+ }
+ const onAbort = function () { childCtl.abort(parent.reason); };
+ parent.addEventListener("abort", onAbort, { once: true });
+ return {
+ signal: childCtl.signal,
+ unlink: function () { parent.removeEventListener("abort", onAbort); }
+ };
+ }
+
+ /**
+ * Aborts the region's live per-delivery signal, called immediately before the NEXT message reaches that region.
+ * A populate that re-populates on every message therefore gets ordering for free: its previous invocation's
+ * messageSignal is already aborted by the time its handler for the new message runs.
+ */
+ function abortPriorMessageSignal(region) {
+ if (region.messageCtl && !region.messageCtl.signal.aborted)
+ region.messageCtl.abort(abortReason(SUPERSEDED_REASON));
+ region.messageCtl = null;
+ }
+
+ // ==================================================================================================================
+ // THE BUS
+ // ==================================================================================================================
+
+ function isDelivering() {
+ return bus.fanOutDepth > 0;
+ }
+
+ /** Resolves an emit's `{to}` to a live region: a fully-qualified key first, then a short id within the host. */
+ function resolveTarget(entry) {
+ for (const r of liveRegions) if (r.key === entry.to) return r;
+ const host = entry.fromRegion ? entry.fromRegion.host : null;
+ if (blank(host)) return null;
+ for (const r of liveRegions) if (r.host === host && r.id === entry.to) return r;
+ return null;
+ }
+
+ function recordHop(entry) {
+ bus.hops.push(entry.from + " -> " + (entry.to == null ? "*" : entry.to));
+ if (bus.hops.length > CYCLE_TRACE_CAP) bus.hops.shift();
+ }
+
+ /**
+ * Reports an emit cycle by printing the SHORTEST REPEATING SUFFIX of the recent hop ring - the actual loop -
+ * rather than the pair of keys that happened to be on the stack when the cap was reached. A cycle is
+ * A -> B -> C -> A and naming the last pair points at an arbitrary edge. No property of any payload is read to
+ * produce this: printing a routing trace is not inspecting a message.
+ */
+ function reportCycle() {
+ window.console.error("JuneauViews.regions: emit cycle detected - one drain queued more than "
+ + BUS_QUEUE_DEPTH_CAP + " emissions from inside its own delivery, and was terminated. Cycle: "
+ + shortestRepeatingSuffix(bus.hops));
+ }
+
+ function shortestRepeatingSuffix(hops) {
+ for (let len = 1; len <= Math.floor(hops.length / 2); len++) {
+ let repeats = true;
+ for (let i = 0; i < len && repeats; i++)
+ if (hops[hops.length - 1 - i] !== hops[hops.length - 1 - i - len]) repeats = false;
+ if (repeats) return hops.slice(hops.length - len).join(" | ");
+ }
+ return hops.slice(Math.max(0, hops.length - 4)).join(" | ");
+ }
+
+ function enqueue(entry) {
+ if (isDelivering() && ++bus.queuedThisDrain > BUS_QUEUE_DEPTH_CAP) {
+ reportCycle();
+ bus.queue.length = 0;
+ return false;
+ }
+ bus.queue.push(entry);
+ return true;
+ }
+
+ function emitEntry(entry) {
+ if (!enqueue(entry)) return;
+ if (barrier.state === "buffering") return;
+ if (isDelivering()) return;
+ drain();
+ }
+
+ function drain() {
+ if (bus.draining) return;
+ bus.draining = true;
+ bus.queuedThisDrain = 0;
+ try {
+ while (bus.queue.length) deliver(bus.queue.shift());
+ } finally {
+ bus.draining = false;
+ bus.queuedThisDrain = 0;
+ }
+ }
+
+ /**
+ * One fan-out: one message to every matching subscriber, synchronously, in subscription order.
+ *
+ * The released-window MEMBERSHIP SNAPSHOT is taken once, here, before any handler runs. Every message delivered
+ * in this fan-out is appended for exactly the windows that were open when it began; a window whose close
+ * condition becomes true partway through remains a member for this fan-out and is closed at its end, and a window
+ * cannot open mid-fan-out because opening one requires the deadline to fire and the deadline is not re-entrant
+ * into a drain. Without the snapshot the buffer's contents would depend on when a promise happened to resolve
+ * inside the drain, and on subscription order, rather than on the message order.
+ */
+ function deliver(entry) {
+ const snapshot = replay.open ? replay.windows.slice() : [];
+ appendForSnapshot(entry, snapshot);
+ const target = entry.to == null ? null : resolveTarget(entry);
+ if (entry.to != null && target == null) {
+ window.console.warn("JuneauViews.regions: no region matches emit target '" + entry.to
+ + "'; the message was not delivered.");
+ return;
+ }
+ if (target != null && target === entry.fromRegion) {
+ window.console.warn("JuneauViews.regions: region '" + entry.from
+ + "' addressed itself; the message was not delivered.");
+ return;
+ }
+ recordHop(entry);
+ bus.fanOutDepth++;
+ try {
+ for (const sub of bus.subscribers.slice()) {
+ if (!subscriberMatches(sub, entry, target)) continue;
+ invokeSubscriber(sub, entry);
+ }
+ } finally {
+ bus.fanOutDepth--;
+ closeDeferredWindows(snapshot);
+ }
+ }
+
+ /**
+ * YOUR OWN BROADCAST DOES NOT COME BACK TO YOU. A self-reflecting control - a date-range card that also displays
+ * "showing last 7 days" - cannot learn its own state from the bus and must keep it in local state, updated in the
+ * same handler that emits.
+ */
+ function subscriberMatches(sub, entry, target) {
+ if (!sub.active) return false;
+ if (target != null) return sub.region === target;
+ return sub.region !== entry.fromRegion;
+ }
+
+ /**
+ * Per-subscriber error isolation: a throw is caught, reported once with both keys, and the loop continues. One
+ * misbehaving consumer must not take the page down, and a repeatedly throwing subscriber is deliberately NOT
+ * auto-unsubscribed - silently dropping it turns a loud bug into a mysterious one.
+ */
+ function invokeSubscriber(sub, entry) {
+ abortPriorMessageSignal(sub.region);
+ try {
+ sub.handler(entry.msg, { from: entry.from, to: entry.to });
+ } catch (e) {
+ window.console.error("JuneauViews.regions: subscriber '" + sub.key + "' threw handling a message from '"
+ + entry.from + "'; the remaining subscribers were unaffected.", e);
+ }
+ }
+
+ // --- the replay window ------------------------------------------------------------------------------------------
+
+ function appendForSnapshot(entry, snapshot) {
+ if (!replay.open || snapshot.length === 0) return;
+ if (replay.buffer.length + 1 > REPLAY_BUFFER_CAP) {
+ abandonReplayBuffer();
+ return;
+ }
+ replay.buffer.push(entry);
+ }
+
+ /**
+ * The buffer's terminal condition. It is ABANDONED, never truncated: a truncated replay violates same-order,
+ * exactly-once BY OMISSION - the region receives a contiguous run with a hole in the middle and no way to know -
+ * which is the one outcome rated worse than no replay at all. The overflow discard SUPERSEDES the membership
+ * snapshot: the overflowing message is not appended for anyone, every window closes immediately rather than at
+ * the fan-out's end (overflow is the buffer's own terminal condition, identical for every member, so there is
+ * nothing for a boundary to make deterministic), and NO buffer is ever reconstituted for the rest of the page
+ * load. Live delivery is untouched and the still-in-flight populates are not aborted.
+ *
+ * It is reported as a data-loss error and NEVER as a cycle: the traffic that reached this cap contains no
+ * repetition, and naming a defect that does not exist is worse than silence.
+ */
+ function abandonReplayBuffer() {
+ const affected = replay.windows.slice();
+ replay.open = false;
+ replay.abandoned = true;
+ replay.buffer = [];
+ replay.windows = [];
+ for (const r of affected) { r.releasedWindowOpen = false; r.windowCloseDeferred = false; }
+ window.console.error("JuneauViews.regions: the initial-drain replay buffer exceeded its cap of "
+ + REPLAY_BUFFER_CAP + " appends and was discarded whole rather than truncated. Delivery exactly-once was"
+ + " NOT met for: " + affected.map(function (r) { return r.key; }).join(", ")
+ + ". The traffic contained no repetition; this is a long-lived released window, not an emit cycle.");
+ }
+
+ function discardReplayBuffer() {
+ replay.open = false;
+ replay.buffer = [];
+ replay.windows = [];
+ }
+
+ function closeReleasedWindow(region) {
+ if (!region.releasedWindowOpen) return;
+ if (isDelivering()) { region.windowCloseDeferred = true; return; }
+ finishWindowClose(region);
+ }
+
+ function finishWindowClose(region) {
+ region.releasedWindowOpen = false;
+ region.windowCloseDeferred = false;
+ const i = replay.windows.indexOf(region);
+ if (i >= 0) replay.windows.splice(i, 1);
+ if (replay.open && replay.windows.length === 0) discardReplayBuffer();
+ }
+
+ function closeDeferredWindows(snapshot) {
+ for (const r of snapshot) if (r.windowCloseDeferred) finishWindowClose(r);
+ }
+
+ /**
+ * Replays the retained initial drain into a newly-subscribing released region. The replay RUNS UNDER THE FAN-OUT
+ * FLAG, so it is governed by the ordinary ordering rules with no second rule to keep in sync: an emit from a
+ * replayed handler is queued rather than live, the queue-depth cap and the cycle diagnostic apply unchanged, and
+ * delivery stays synchronous within the ctx.on call.
+ */
+ function replayInto(sub) {
+ const buffered = replay.buffer.slice();
+ bus.fanOutDepth++;
+ try {
+ for (const entry of buffered) {
+ const target = entry.to == null ? null : resolveTarget(entry);
+ if (!subscriberMatches(sub, entry, target)) continue;
+ invokeSubscriber(sub, entry);
+ }
+ } finally {
+ bus.fanOutDepth--;
+ }
+ drain();
+ }
+
+ // --- the ctx-scoped subscription --------------------------------------------------------------------------------
+
+ function subscribe(region, handler) {
+ if (typeof handler !== "function") {
+ window.console.error("JuneauViews.regions: ctx.on(handler) requires a function.");
+ return function () { /* nothing was subscribed, so nothing is unsubscribed */ };
+ }
+ const sub = { region: region, key: region.key, handler: handler, active: true };
+ bus.subscribers.push(sub);
+ region.subs.push(sub);
+ if (replay.open && region.releasedWindowOpen && !region.replayed) {
+ region.replayed = true;
+ replayInto(sub);
+ }
+ return function () { unsubscribeOne(sub); };
+ }
+
+ function unsubscribeOne(sub) {
+ sub.active = false;
+ const i = bus.subscribers.indexOf(sub);
+ if (i >= 0) bus.subscribers.splice(i, 1);
+ }
+
+ /**
+ * Emits from a region. The payload is OPAQUE: the framework moves the object reference and never reads a
+ * property of it - no normalizing, no kind-based routing, no cloning, no serialization. Two consequences the
+ * javadoc has to carry rather than defend against: the framework cannot warn about a message nobody handles,
+ * because it cannot tell a broadcast meant to be ignored from a misaddressed one; and payload identity is SHARED,
+ * so a subscriber that mutates the payload affects the others. Copying would require inspecting the payload, and
+ * a structured clone would break a payload carrying a DOM node or a function - both of which a consumer may
+ * legitimately send.
+ *
+ * `{to}` re-couples two regions by id, which is the coupling the bus exists to remove. Reach for broadcast; use
+ * `{to}` when you genuinely mean "drive that one".
+ */
+ function emitFrom(region, msg, opts) {
+ const to = opts?.to != null ? String(opts.to) : null;
+ emitEntry({ msg: msg, from: region.key, to: to, fromRegion: region });
+ }
+
+ /**
+ * Emits a framework-authored message. `meta.from` names the framework rather than a region, so a broadcast
+ * reaches every subscriber including the one whose chrome caused it.
+ *
+ * This is the CHROME half of the ownership invariant: Juneau-drawn chrome auto-emits, author-drawn content emits
+ * explicitly, and the framework never auto-emits for content it did not draw. There is no framework listener
+ * anywhere inside a region container, and there must never be one - a "helpful" click delegate over author DOM
+ * would make the framework-emitted set open-ended and break the invariant at the click level. The dividing line
+ * is drawn where knowledge actually is: the framework is the authoritative source for selection, expansion and
+ * redraw, and would have to guess at everything inside a region container.
+ */
+ function emitFramework(msg, opts) {
+ const to = opts?.to != null ? String(opts.to) : null;
+ emitEntry({ msg: msg, from: FRAMEWORK_SENDER_KEY, to: to, fromRegion: null });
+ }
+
+ // --- the closed set of framework message schemas ------------------------------------------------------------------
+
+ /**
+ * `viewId` is mandatory on all three schemas. Broadcast plus two tables on one page otherwise leaves a
+ * subscriber unable to tell WHOSE selection arrived, and `meta.from` cannot disambiguate it either: for a
+ * framework-emitted message `meta.from` names the framework, so there is no host for a short target to resolve
+ * against. The identity has to be in the payload, which is why the framework authoring these payloads is
+ * load-bearing rather than incidental. `schemaVersion` is per-message and independent of the region contract
+ * version, for the same reason the envelopes are independent: adding a field here must not force a
+ * region-envelope bump.
+ */
+ function selectionChangedMessage(o) {
+ return {
+ kind: FRAMEWORK_KIND_PREFIX + "selection-changed",
+ schemaVersion: 1,
+ viewId: o.viewId,
+ ids: o.ids || [],
+ rows: o.rows || [],
+ added: o.added || [],
+ removed: o.removed || []
+ };
+ }
+
+ function detailToggledMessage(o) {
+ return {
+ kind: FRAMEWORK_KIND_PREFIX + "detail-toggled",
+ schemaVersion: 1,
+ viewId: o.viewId,
+ rowId: o.rowId,
+ expanded: !!o.expanded,
+ generation: o.generation
+ };
+ }
+
+ function tableRedrewMessage(o) {
+ return {
+ kind: FRAMEWORK_KIND_PREFIX + "table-redrew",
+ schemaVersion: 1,
+ viewId: o.viewId,
+ nested: !!o.nested,
+ rowCount: o.rowCount,
+ page: o.page
+ };
+ }
+
+ // ==================================================================================================================
+ // THE BARRIER
+ // ==================================================================================================================
+
+ /**
+ * Declares a runtime present on the page. Presence is established by the asset loading and registering itself at
+ * parse time rather than by a document scan, so there is no list to keep in sync.
+ */
+ function registerRuntime(token) {
+ if (blank(token)) return;
+ if (barrier.runtimes.indexOf(token) < 0) barrier.runtimes.push(token);
+ }
+
+ /**
+ * Declares that a runtime has finished its enrolment walk. Enrolment completion is DECLARED by the host runtime,
+ * not inferred from the clock: the enrolment walk is synchronous, so the set is complete for a host the moment
+ * that host's own init returns, which is what makes the declaration a statement a runtime can truthfully make.
+ */
+ function ready(token) {
+ if (blank(token)) return;
+ if (barrier.ready.indexOf(token) < 0) barrier.ready.push(token);
+ maybeLift();
+ }
+
+ /**
+ * Arms the barrier at the FIRST enrolment, never from a document scan - so a page with no regions never arms it,
+ * adds no listener and starts no timer.
+ */
+ function armBarrier() {
+ if (barrier.state !== "unarmed") return;
+ barrier.state = "buffering";
+ barrier.deadlineTimer = window.setTimeout(onDeadline, BARRIER_DEADLINE_MS);
+ armBackstop();
+ }
+
+ /**
+ * The backstop under the declared-ready protocol, and what makes declaring safe to rely on. A registered runtime
+ * that never reports has the barrier lift anyway at DOMContentLoaded plus one macrotask, with one error naming it
+ * - so a forgetful runtime degrades loudly rather than silently, which is the inverse of the failure mode the
+ * clock-only alternative was rejected for.
+ */
+ function armBackstop() {
+ if (barrier.backstopArmed) return;
+ barrier.backstopArmed = true;
+ const fire = function () { window.setTimeout(backstopCheck, 0); };
+ if (window.document.readyState === "loading") window.document.addEventListener("DOMContentLoaded", fire);
+ else fire();
+ }
+
+ function backstopCheck() {
+ if (barrier.state !== "buffering") return;
+ const missing = barrier.runtimes.filter(function (t) { return barrier.ready.indexOf(t) < 0; });
+ if (missing.length === 0) return;
+ window.console.error("JuneauViews.regions: lifting the initial-broadcast barrier without an enrolment report"
+ + " from: " + missing.join(", ") + ". A runtime that enrols regions must call"
+ + " JuneauViews.regions.ready(...) when its walk completes.");
+ for (const t of missing) barrier.ready.push(t);
+ maybeLift();
+ }
+
+ /** Enters a region into the outstanding set. Only a region whose FIRST populate is an initial one is a member. */
+ function enterBarrier(region) {
+ if (barrier.state !== "buffering") return;
+ region.barrierMember = true;
+ barrier.outstanding.push(region);
+ }
+
+ function leaveBarrier(region) {
+ if (!region.barrierMember) return;
+ region.barrierMember = false;
+ const i = barrier.outstanding.indexOf(region);
+ if (i >= 0) barrier.outstanding.splice(i, 1);
+ maybeLift();
+ }
+
+ function maybeLift() {
+ if (barrier.state !== "buffering") return;
+ if (barrier.outstanding.length) return;
+ if (barrier.runtimes.some(function (t) { return barrier.ready.indexOf(t) < 0; })) return;
+ lift();
+ }
+
+ function lift() {
+ barrier.state = "lifted";
+ if (barrier.deadlineTimer !== null) {
+ window.clearTimeout(barrier.deadlineTimer);
+ barrier.deadlineTimer = null;
+ }
+ drain();
+ }
+
+ /**
+ * The deadline fires in two phases. Phase one drains NOW, so the healthy regions are not made to wait on the
+ * slow one - that is the whole point of having a deadline. Phase two gives each released region a bounded window
+ * in which its later ctx.on receives the drain it missed, once, whole and in the original order, so a released
+ * region ends in the state it would have reached had it settled on time.
+ *
+ * A released populate is LEFT RUNNING - not aborted, signal not fired, container untouched - and may still
+ * resolve and paint. The barrier is an ordering device, not a lifecycle one; giving it the power to cancel a
+ * populate would turn a slow network into a cancelled paint, which is far worse than a late-delivered first
+ * message. One warning for the page, not one per region.
+ */
+ function onDeadline() {
+ barrier.deadlineTimer = null;
+ if (barrier.state !== "buffering") return;
+ const released = barrier.outstanding.slice();
+ barrier.outstanding.length = 0;
+ for (const r of released) r.barrierMember = false;
+ if (released.length && !replay.abandoned) {
+ replay.open = true;
+ replay.buffer = [];
+ replay.windows = released.slice();
+ for (const r of released) r.releasedWindowOpen = true;
+ }
+ if (released.length && !barrier.warned) {
+ barrier.warned = true;
+ window.console.warn("JuneauViews.regions: the initial-broadcast barrier reached its "
+ + BARRIER_DEADLINE_MS + "ms deadline and released: "
+ + released.map(function (r) { return r.key; }).join(", ")
+ + ". Their populates are still running and may still paint.");
+ }
+ for (const t of barrier.runtimes) if (barrier.ready.indexOf(t) < 0) barrier.ready.push(t);
+ lift();
+ }
+
+ // ==================================================================================================================
+ // THE ctx
+ // ==================================================================================================================
+
+ /**
+ * Builds the exact ctx contract. This literal IS the contract - a member introduced anywhere else has not been
+ * introduced - so it is written as one object literal rather than assembled across several assignments, and its
+ * member set is pinned by a golden test.
+ */
+ function buildCtx(region, reason) {
+ return {
+ contractVersion: REGION_CONTRACT_VERSION,
+ type: region.type,
+ id: region.id,
+ key: region.key,
+ ids: region.ids,
+ params: region.params,
+ declared: region.declared,
+ data: region.data,
+ fetchDeclared: function () { return fetchDeclared(region); },
+ defaultPopulate: defaultPopulate,
+ write: function (url, body, opts) { return regionWrite(region, url, body, opts); },
+ selection: region.selection,
+ emit: function (msg, opts) { emitFrom(region, msg, opts); },
+ on: function (handler) { return subscribe(region, handler); },
+ helpers: NS.helpers,
+ host: region.host,
+ generation: region.generation,
+ signal: region.signalCtl.signal,
+ messageSignal: reason === "message" ? region.messageCtl.signal : null,
+ reason: reason,
+ refresh: function () { requestRefresh(region); }
+ };
+ }
+
+ /**
+ * Performs the region's declared fetch, using the current invocation's signal. A region that declared no dataUrl
+ * has nothing to fetch, so this resolves null - which is every region under the identity-only descriptor this
+ * runtime reads today. It does not touch the region's loading/ok/error state: a fetch is not a paint.
+ */
+ function fetchDeclared(region) {
+ if (blank(region.declared.dataUrl)) return Promise.resolve(null);
+ return Promise.reject(new Error("JuneauViews.regions: ctx.fetchDeclared() cannot run a declared fetch for '"
+ + region.key + "' because no declarative region descriptor is served to this runtime."));
+ }
+
+ /**
+ * Awaited delegation to the reserved default. Exposed on ctx as well as on the namespace so a wrapper does not
+ * have to reach for a global. An absent default fails VISIBLY, exactly as an unknown populator name does, rather
+ * than resolving quietly and leaving a blank container behind.
+ */
+ function defaultPopulate(ctx, container) {
+ const fn = resolve(DEFAULT_POPULATOR_NAME);
+ if (typeof fn !== "function") {
+ const message = "no populator is registered under the reserved name '" + DEFAULT_POPULATOR_NAME + "'";
+ window.console.error("JuneauViews.regions: region '" + (ctx ? ctx.key : "?")
+ + "' delegated to the default and " + message + ".");
+ return Promise.reject(new Error(message));
+ }
+ return Promise.resolve(fn(ctx, container));
+ }
+
+ /**
+ * The framework-owned, CSRF-stamped write. It exists because a populate that paints a button has no other way to
+ * reach the token: the token is stamped on a host element and read back by the view runtime with a bare
+ * getAttribute, and a card grid carries no table. Everything here mirrors the row-action write path rather than
+ * inventing a second one - the same safe-method refusal, the same non-normalizing blank-token test, the same
+ * Content-Type stamp that keeps the write inside the server's JSON boundary check, and the same
+ * refusal-marker-versus-response split.
+ *
+ * Two things it deliberately is NOT. It is not a general HTTP client - a populate that wants an arbitrary
+ * cross-origin fetch writes one, gets no token, and that is the correct outcome. And it does not replace the
+ * row-action and declarative-modal submit paths, which have their own typed result settling; this is the REGION
+ * write, for a button a populate painted itself.
+ *
+ * It does not touch the region's loading/ok/error state, in either direction and including on a refusal: a write
+ * is not a paint.
+ */
+ function regionWrite(region, url, body, opts) {
+ const o = opts || {};
+ const init = NS.init;
+ if (typeof init?.isSafeMethod !== "function" || typeof init.isSafeDetailUrl !== "function"
+ || typeof init.isBlankToken !== "function" || typeof init.resolveCsrfToken !== "function"
+ || typeof init.resolveCsrfHeaderName !== "function")
+ return refuseWrite(region, "runtime-unavailable", "ctx.write cannot run because the view runtime's write"
+ + " guards are unavailable; juneau-regions.js must load after juneau-views.js.");
+ const method = String(o.method || "POST").toUpperCase();
+ if (init.isSafeMethod(method))
+ return refuseWrite(region, "safe-method", "ctx.write refuses the safe method '" + method
+ + "'; a read is a fetch or ctx.fetchDeclared(), not a write.");
+ if (!init.isSafeDetailUrl(url))
+ return refuseWrite(region, "unsafe-url", "ctx.write refuses '" + url
+ + "': a write URL must be a same-origin path with no scheme and no '..' segment.");
+ const host = region.el.closest("[data-juneau-csrf]");
+ const token = host ? init.resolveCsrfToken(host) : null;
+ if (init.isBlankToken(token))
+ return refuseWrite(region, "missing-token", "no CSRF token available - the page did not supply one, so"
+ + " the request was not sent.");
+ const headers = {};
+ for (const k of keysOf(o.headers)) headers[k] = o.headers[k];
+ headers["Content-Type"] = "application/json";
+ headers[init.resolveCsrfHeaderName(host)] = token;
+ const payload = {};
+ for (const k of keysOf(body)) payload[k] = body[k];
+ if (o.idempotencyKey != null) payload.idempotencyKey = o.idempotencyKey;
+ return sendWrite(region, url, method, headers, payload, o.signal);
+ }
+
+ /**
+ * Issues the request under `opts.signal` when one was given and under a FORK otherwise - never under the bare
+ * ctx.signal, so two rapid writes from the same region cancel the first rather than racing. Forking by default
+ * is what makes it impossible for an author to reproduce the stale-response overwrite here by forgetting.
+ */
+ function sendWrite(region, url, method, headers, payload, signal) {
+ return fetch(url, {
+ method: method,
+ headers: headers,
+ body: JSON.stringify(payload),
+ signal: signal || region.ctx.signal.fork()
+ }).then(function (res) {
+ return res.text().then(function (text) {
+ let data = null;
+ try { data = text ? JSON.parse(text) : null; } catch (e) { data = null; }
+ return { ok: res.ok, status: res.status, data: data };
+ });
+ });
+ }
+
+ /**
+ * A refusal is a DIFFERENT KIND OF EVENT from a 500, and collapsing them is how a caller ends up retrying
+ * something that was never sent - so a refusal rejects with a marker, renders a visible message, and issues zero
+ * network traffic. The message is painted as its own framework-owned alert rather than through the region's
+ * status, because a write must not move the region's loading/ok/error state.
+ */
+ function refuseWrite(region, reason, message) {
+ window.console.error("JuneauViews.regions: region '" + region.key + "' - " + message);
+ const p = window.document.createElement("p");
+ p.setAttribute("data-juneau-region-write-error", "");
+ p.setAttribute("role", "alert");
+ p.textContent = message;
+ region.el.appendChild(p);
+ const e = new Error(message);
+ e.reason = reason;
+ return Promise.reject(e);
+ }
+
+ // ==================================================================================================================
+ // THE PRIMITIVE
+ // ==================================================================================================================
+
+ /**
+ * Mints a region from its container. `key` is the framework's own stable, opaque identity for the region -
+ * unique on the page and the value a fully-qualified emit target names - which is what makes a second expanded
+ * row's region a different subscriber from the first's.
+ */
+ function mintRegion(el) {
+ const id = el.getAttribute(REGION_ATTR) || "";
+ const declaredType = el.getAttribute(REGION_TYPE_ATTR);
+ const host = el.getAttribute(REGION_HOST_ATTR) || "";
+ return {
+ el: el,
+ id: id,
+ type: REGION_TYPES.indexOf(declaredType) >= 0 ? declaredType : "card-body",
+ host: host,
+ key: host ? host + "/" + id : id,
+ ids: readIds(el),
+ params: {},
+ declared: { dataUrl: null, renderer: null, lazy: false, refreshMs: null, fields: null, titleFields: null },
+ data: null,
+ selection: null,
+ populateName: el.getAttribute(REGION_POPULATE_ATTR),
+ generation: 0,
+ state: "idle",
+ ctx: null,
+ signalCtl: null,
+ lastFork: null,
+ lastForkUnlink: null,
+ messageCtl: null,
+ cleanup: null,
+ pollTimer: null,
+ inFlight: null,
+ invoking: false,
+ pendingReason: null,
+ selfRefreshed: false,
+ subs: [],
+ deferred: false,
+ barrierMember: false,
+ releasedWindowOpen: false,
+ windowCloseDeferred: false,
+ replayed: false,
+ firstPopulateDone: false,
+ torn: false
+ };
+ }
+
+ /**
+ * The enclosing identities, as a FLAT BAG of nullable fields rather than a union. Deliberate: a consumer that
+ * only wants a row id writes ctx.ids.rowId regardless of region type, and the framework hands over what it
+ * already knows instead of making the consumer re-derive it by walking the DOM. `sectionId` stays in the shape
+ * and is null on a row-detail region: a detail body is one region rather than a strip of sections, so there is no
+ * enclosing section to name, and an author reading it and getting the region's own id back would conclude
+ * otherwise.
+ */
+ function readIds(el) {
+ const read = function (attr) {
+ const host = el.closest("[" + attr + "]");
+ return host ? host.getAttribute(attr) : null;
+ };
+ return {
+ viewId: read("data-juneau-view"),
+ rowId: read("data-juneau-row-id"),
+ pageId: read("data-juneau-page"),
+ tabId: read("data-panel-tab"),
+ subtabId: read("data-panel-subtab"),
+ gridId: read("data-juneau-card-grid"),
+ cardId: read("data-juneau-card"),
+ sectionId: null
+ };
+ }
+
+ /**
+ * Enrols one region container.
+ *
+ * IDEMPOTENT PER NODE, which is a copy of the table initializer's survives-the-settle discipline rather than a
+ * new one: the first call mints the region and marks the node, and a second call on a node that already carries a
+ * live region is a NO-OP returning the existing handle - no second populate, no second subscription, no second
+ * teardown entry, no reason transition. So a second enrolment walk over the same panel enrols nothing new and a
+ * consumer's in-place state survives a tab round-trip. The mark is cleared by teardown and by nothing else,
+ * deliberately NOT on settle: an implementer copying the in-flight-pending marker's lifecycle instead would clear
+ * it when the first populate returned, and the next walk would re-mint and re-populate every region on the page.
+ *
+ * ENROLMENT IS NOT BARRIER MEMBERSHIP. A region visible at enrolment populates with reason "initial" and enters
+ * the barrier's outstanding set; a region enrolled HIDDEN is enrolled, owned and torn-down-able, defers its first
+ * populate, and takes reason "activate" when it becomes visible - so it never has an initial populate at all and
+ * was never a barrier member. Otherwise a card in a tab the user never opens would hold the whole page's initial
+ * broadcast until the deadline, and every tabbed card page would wait out the full deadline before its VISIBLE
+ * card received anything.
+ *
+ * A marked node found DISCONNECTED is a loud named error rather than a silent no-op: a region whose node left the
+ * document without a teardown is a bug. The detection is real but partial - a host detached and never
+ * reinserted and never re-enrolled is invisible here, because this function is never called again for it.
+ */
+ function initRegion(el) {
+ if (!isElement(el)) {
+ window.console.error("JuneauViews.regions.initRegion: expected a region container element.");
+ return null;
+ }
+ const existing = el._juneauRegion;
+ if (existing) {
+ if (!isConnected(el))
+ window.console.error("JuneauViews.regions: region '" + existing.key
+ + "' is still marked but its container is no longer in the document; it was removed without a"
+ + " teardown, so its subscription and its signal are leaked.");
+ return existing;
+ }
+ if (!HAS_ABORT_CONTROLLER) {
+ refuseMissingBaseline(el);
+ return null;
+ }
+ const region = mintRegion(el);
+ el._juneauRegion = region;
+ liveRegions.push(region);
+ el.setAttribute(REGION_STATE_ATTR, "idle");
+ armBarrier();
+ if (isRegionHidden(el)) {
+ region.deferred = true;
+ return region;
+ }
+ enterBarrier(region);
+ runPopulate(region, "initial");
+ return region;
+ }
+
+ /**
+ * The baseline refusal. A blank container with no error is the worst failure this runtime has, and a silently
+ * signal-less region is a subtler version of it - so every region gets the error state, and the PAGE gets exactly
+ * one console error naming the missing platform feature rather than one per region.
+ */
+ function refuseMissingBaseline(el) {
+ const message = "the region runtime requires AbortController, which this environment does not provide;"
+ + " no region on this page will populate.";
+ if (!baselineReported) {
+ baselineReported = true;
+ window.console.error("JuneauViews.regions: " + message);
+ }
+ el.setAttribute(REGION_STATE_ATTR, "error");
+ const render = NS.init?.renderAsyncStatus;
+ if (typeof render === "function") render(el, "error", message);
+ }
+
+ /**
+ * Runs the deferred first populate for a region that has just become visible. Its reason is "activate" and never
+ * "initial", and it is not a barrier member, so it receives nothing retroactively - not the initial broadcast,
+ * and not a replay even if a released region's window happens to be open at that moment.
+ */
+ function activateRegion(region) {
+ if (region.torn || region.firstPopulateDone || !region.deferred) return;
+ if (!isConnected(region.el) || isRegionHidden(region.el)) return;
+ region.deferred = false;
+ runPopulate(region, "activate");
+ }
+
+ /**
+ * Walks `scopeEl` for region containers and enrols each one. SYNCHRONOUS and complete in its caller's own turn:
+ * it enrols, it does not populate-and-wait, so a host runtime can truthfully declare its walk finished the moment
+ * its own init returns. Returns the handles it minted, so a caller that keeps its own ownership record pushes
+ * them itself rather than having this walk reach into a private structure it does not own.
+ */
+ function enrolIn(scopeEl) {
+ const scope = scopeEl || window.document.body;
+ if (typeof scope?.querySelectorAll !== "function") return [];
+ const out = [];
+ for (const el of scope.querySelectorAll("[" + REGION_ATTR + "]")) {
+ const handle = initRegion(el);
+ if (handle) out.push(handle);
+ }
+ return out;
+ }
+
+ /**
+ * Invokes the populator.
+ *
+ * The framework CLEARS THE CONTAINER BEFORE EVERY CALL, which makes "populate again" trivially correct and
+ * removes the class of bug where a re-populate appends a second copy. A consumer that wants update-in-place
+ * instead does it from a bus handler and never re-populates. A populate in flight is NOT re-entered: a second
+ * trigger aborts the current invocation's signal, waits for the settle, and then runs once.
+ *
+ * A throw is CONTAINED - the region goes to its error state and its siblings stay healthy - and a throw counts as
+ * a settle for the barrier in every one of its three forms: a synchronous throw, a rejected promise, and a
+ * promise that never settles at all (released at the deadline).
+ */
+ function runPopulate(region, reason) {
+ if (region.torn) return;
+ if (region.inFlight || region.invoking) {
+ region.pendingReason = reason;
+ if (region.inFlight) region.signalCtl.abort(abortReason(REPOPULATE_REASON));
+ return;
+ }
+ const fn = resolvePopulator(region);
+ if (!fn) return;
+ disposeInvocation(region, REPOPULATE_REASON);
+ beginInvocation(region, reason);
+ setRegionState(region, "loading");
+ let returned;
+ region.invoking = true;
+ try {
+ returned = fn(region.ctx, region.el);
+ } catch (e) {
+ region.invoking = false;
+ failPopulate(region, e);
+ return;
+ }
+ region.invoking = false;
+ if (typeof returned?.then === "function") {
+ region.inFlight = returned;
+ returned.then(
+ function (v) { settlePopulate(region, v); },
+ function (e) { failPopulate(region, e); });
+ return;
+ }
+ settlePopulate(region, returned);
+ }
+
+ /**
+ * An unknown populator name fails VISIBLY rather than silently - a region has no raw value to fall back to, and a
+ * blank container with no error reads as "no data" rather than "broken page". The region leaves the barrier as
+ * it would on any other settle, so one bad name cannot hold the page's initial broadcast.
+ */
+ function resolvePopulator(region) {
+ const name = region.populateName == null ? DEFAULT_POPULATOR_NAME : region.populateName;
+ const fn = resolve(name);
+ if (typeof fn === "function") return fn;
+ const message = "no populator is registered under the name '" + name + "'.";
+ window.console.error("JuneauViews.regions: region '" + region.key + "' - " + message);
+ clearContainer(region.el);
+ setRegionState(region, "error", message);
+ region.firstPopulateDone = true;
+ leaveBarrier(region);
+ closeReleasedWindow(region);
+ return null;
+ }
+
+ function beginInvocation(region, reason) {
+ region.signalCtl = new AbortController();
+ region.lastFork = null;
+ region.lastForkUnlink = null;
+ attachFork(region, region.signalCtl.signal);
+ if (reason === "message") {
+ region.messageCtl = new AbortController();
+ linkToParent(region.signalCtl.signal, region.messageCtl);
+ }
+ region.ctx = buildCtx(region, reason);
+ }
+
+ function settlePopulate(region, returned) {
+ region.inFlight = null;
+ if (typeof returned === "function") region.cleanup = returned;
+ if (region.state !== "error") setRegionState(region, "ok");
+ finishInvocation(region);
+ }
+
+ /**
+ * A rejected promise is a contained throw and registers NO cleanup - a populate that failed before it finished
+ * allocating cannot be trusted to know what to release.
+ */
+ function failPopulate(region, e) {
+ region.inFlight = null;
+ region.cleanup = null;
+ clearContainer(region.el);
+ setRegionState(region, "error", e?.message ? String(e.message) : "the region's populate failed.");
+ window.console.error("JuneauViews.regions: region '" + region.key + "' - its populate failed; the region is"
+ + " in its error state and its siblings are unaffected.", e);
+ finishInvocation(region);
+ }
+
+ function finishInvocation(region) {
+ region.firstPopulateDone = true;
+ leaveBarrier(region);
+ closeReleasedWindow(region);
+ const pending = region.pendingReason;
+ if (pending != null) {
+ region.pendingReason = null;
+ runPopulate(region, pending);
+ }
+ }
+
+ /**
+ * Asks the framework to re-invoke this region's populate, so re-entrancy stays the framework's problem rather
+ * than a consumer's recursive call. `reason` tracks the CAUSAL EVENT, not the API that was invoked: a refresh
+ * requested synchronously inside a bus fan-out arrives as "message", and the identical call from a timer or a
+ * click arrives as "refresh". The field exists so an author can animate a data-driven update differently from a
+ * first paint, and "a message changed my inputs" is the distinction that matters - not which framework function
+ * was on the stack.
+ *
+ * A refresh requested from INSIDE a populate is legal and is coalesced: it runs once after the settle, and a
+ * second in-populate refresh from that re-run is dropped with a warning, so a populate that calls refresh()
+ * unconditionally spins exactly once rather than forever. A later refresh from outside a populate re-arms the
+ * one permitted spin.
+ */
+ function requestRefresh(region) {
+ const reason = isDelivering() ? "message" : "refresh";
+ if (region.invoking) {
+ if (region.selfRefreshed) {
+ window.console.warn("JuneauViews.regions: region '" + region.key + "' called ctx.refresh() from"
+ + " inside the populate its own previous refresh triggered; the second spin was coalesced away."
+ + " A populate that refreshes itself unconditionally spins once, not forever.");
+ return;
+ }
+ region.selfRefreshed = true;
+ region.pendingReason = reason;
+ return;
+ }
+ region.selfRefreshed = false;
+ runPopulate(region, reason);
+ }
+
+ /**
+ * Teardown steps 0 through 5, in a FIXED order, on collapse, destroy, eviction or re-populate.
+ *
+ * Step 0 - the declared poll timer - precedes the abort deliberately: aborting while a timer is still armed
+ * leaves a tick that fires into a region already being torn down. Steps 3 and 4 are the ones a consumer cannot
+ * get right on its own, which is why they are the framework's: a region that is going away should not keep
+ * receiving messages, and should not drive the page after it is gone. The order is asserted by an
+ * order-recording test, because a plausible refactor reorders it silently.
+ */
+ function disposeInvocation(region, reason) {
+ // 0. Clear the region's declared poll timer.
+ if (region.pollTimer !== null) {
+ window.clearTimeout(region.pollTimer);
+ region.pollTimer = null;
+ }
+ // 1. Abort ctx.signal, which transitively aborts every fork() child and any live messageSignal.
+ if (region.signalCtl && !region.signalCtl.signal.aborted) region.signalCtl.abort(abortReason(reason));
+ region.messageCtl = null;
+ // 2. Invoke the cleanup function, if one was returned - including one resolved from a Promise.
+ if (typeof region.cleanup === "function") {
+ const cleanup = region.cleanup;
+ region.cleanup = null;
+ try { cleanup(); } catch (e) {
+ window.console.error("JuneauViews.regions: region '" + region.key + "' - its cleanup function threw;"
+ + " the rest of the teardown ran anyway.", e);
+ }
+ }
+ // 3. Unsubscribe every handler the region registered through ctx.on.
+ for (const sub of region.subs.slice()) unsubscribeOne(sub);
+ region.subs.length = 0;
+ // 4. Discard any message this region queued that has not yet been delivered.
+ bus.queue = bus.queue.filter(function (e) { return e.fromRegion !== region; });
+ // 5. Clear the container.
+ clearContainer(region.el);
+ }
+
+ /** Tears a region down for good: the invocation steps, then the marks and the page-scoped bookkeeping. */
+ function teardownRegion(region) {
+ if (!region || region.torn) return;
+ region.torn = true;
+ disposeInvocation(region, TEARDOWN_REASON);
+ region.el.removeAttribute(REGION_STATE_ATTR);
+ delete region.el._juneauRegion;
+ const i = liveRegions.indexOf(region);
+ if (i >= 0) liveRegions.splice(i, 1);
+ leaveBarrier(region);
+ closeReleasedWindow(region);
+ }
+
+ /**
+ * Sweeps every region under `root`, `root` itself included. IDEMPOTENT over an already-swept root - a second
+ * sweep finds no mark and does nothing - because the sweep sites nest: a table teardown sweeps a subtree that a
+ * nested table's own teardown then sweeps again.
+ */
+ function teardownRegionsIn(root) {
+ if (typeof root?.querySelectorAll !== "function") return;
+ if (root._juneauRegion) teardownRegion(root._juneauRegion);
+ for (const el of root.querySelectorAll("[" + REGION_ATTR + "]"))
+ if (el._juneauRegion) teardownRegion(el._juneauRegion);
+ }
+
+ // ==================================================================================================================
+ // EXPORTS
+ // ==================================================================================================================
+
+ NS.regions = {
+ CONTRACT_VERSION: REGION_CONTRACT_VERSION,
+ REGION_ATTR: REGION_ATTR,
+ REGION_TYPE_ATTR: REGION_TYPE_ATTR,
+ REGION_HOST_ATTR: REGION_HOST_ATTR,
+ REGION_POPULATE_ATTR: REGION_POPULATE_ATTR,
+ REGION_STATE_ATTR: REGION_STATE_ATTR,
+ REGION_TYPES: REGION_TYPES,
+ RESERVED_POPULATOR_NAMES: RESERVED_POPULATOR_NAMES,
+ FRAMEWORK_KIND_PREFIX: FRAMEWORK_KIND_PREFIX,
+ FRAMEWORK_SENDER_KEY: FRAMEWORK_SENDER_KEY,
+ BUS_QUEUE_DEPTH_CAP: BUS_QUEUE_DEPTH_CAP,
+ REPLAY_BUFFER_CAP: REPLAY_BUFFER_CAP,
+ BARRIER_DEADLINE_MS: BARRIER_DEADLINE_MS,
+ register: register,
+ resolve: resolve,
+ initRegion: initRegion,
+ activateRegion: activateRegion,
+ enrolIn: enrolIn,
+ teardownRegion: teardownRegion,
+ teardownRegionsIn: teardownRegionsIn,
+ registerRuntime: registerRuntime,
+ ready: ready,
+ defaultPopulate: defaultPopulate,
+ builtins: builtins,
+ emitFramework: emitFramework,
+ selectionChangedMessage: selectionChangedMessage,
+ detailToggledMessage: detailToggledMessage,
+ tableRedrewMessage: tableRedrewMessage
+ };
+})();
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/PopulatorAllowlist_Test.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/PopulatorAllowlist_Test.java
new file mode 100644
index 0000000000..bd09c444ae
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/PopulatorAllowlist_Test.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.views;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.nio.charset.*;
+import java.nio.file.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Java name-allowlist for region populators, plus a source-level guard on the class Javadoc's honest-limit warning.
+ */
+class PopulatorAllowlist_Test extends TestBase {
+
+ @Test void a01_builtinAccepted() {
+ PopulatorAllowlist.assertAllowed("default", null);
+ }
+
+ @Test void a02_optedInCustomAccepted() {
+ PopulatorAllowlist.assertAllowed("myWidget", Set.of("myWidget"));
+ }
+
+ @Test void a03_unknownRejected() {
+ var e = assertThrows(IllegalArgumentException.class,
+ () -> PopulatorAllowlist.assertAllowed("evil", null));
+ assertTrue(e.getMessage().contains("evil"), e::getMessage);
+ }
+
+ @Test void a04_customRejectedWithoutOptIn() {
+ assertThrows(IllegalArgumentException.class,
+ () -> PopulatorAllowlist.assertAllowed("myWidget", null));
+ }
+
+ @Test void a05_nullAndBlankRejected() {
+ assertThrows(IllegalArgumentException.class, () -> PopulatorAllowlist.assertAllowed(null, null));
+ assertThrows(IllegalArgumentException.class, () -> PopulatorAllowlist.assertAllowed("", null));
+ assertThrows(IllegalArgumentException.class, () -> PopulatorAllowlist.assertAllowed(" ", null));
+ }
+
+ @Test void a06_nullAllowedNamesTolerated() {
+ PopulatorAllowlist.assertAllowed("default", null);
+ }
+
+ @Test void a07_builtinIds_isExactlyDefault() {
+ assertEquals(Set.of("default"), PopulatorAllowlist.BUILTIN_IDS);
+ }
+
+ /** Locates {@code PopulatorAllowlist.java}'s own source on disk, the same way {@code locateHarness()} does in
+ * {@link ViewsJs_ContractVersion_Test}. */
+ private static Path locateSource() {
+ var basedir = System.getProperty("basedir");
+ if (basedir != null) {
+ var p = Path.of(basedir, "src/main/java/org/apache/juneau/rest/server/views/PopulatorAllowlist.java");
+ if (Files.isRegularFile(p)) return p.toAbsolutePath().normalize();
+ }
+ for (var rel : List.of(
+ "src/main/java/org/apache/juneau/rest/server/views/PopulatorAllowlist.java",
+ "juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/PopulatorAllowlist.java")) {
+ var p = Path.of(rel);
+ if (Files.isRegularFile(p)) return p.toAbsolutePath().normalize();
+ }
+ return null;
+ }
+
+ /**
+ * Guards the honest-limit warning in {@link PopulatorAllowlist}'s class Javadoc against silent deletion by a
+ * future editor. Unlike {@link SinkRenderAllowlist}, a populator allowlist controls only which name is
+ * resolved and nothing about what the resolved populator then does — a fact that is easy to lose once the
+ * class exists and looks, on its shape alone, exactly like a real security boundary. This test reads the
+ * class's own source file off disk and fails if either load-bearing phrase is missing, so deleting the warning
+ * (rather than merely editing code around it) breaks the build.
+ */
+ @Test void a08_classJavadoc_keepsTheHonestLimitWarning() throws IOException {
+ var src = locateSource();
+ assertNotNull(src, "could not locate PopulatorAllowlist.java on disk to check its class Javadoc");
+ var body = Files.readString(src, StandardCharsets.UTF_8);
+ var classStart = body.indexOf("public final class PopulatorAllowlist");
+ assertTrue(classStart > 0, () -> "could not find 'public final class PopulatorAllowlist' in:\n" + src);
+ var javadoc = body.substring(0, classStart).toLowerCase(Locale.ROOT);
+ assertTrue(javadoc.contains("authoring-discipline") || javadoc.contains("authoring discipline"),
+ () -> "PopulatorAllowlist's class Javadoc must keep stating that this allowlist is an authoring-discipline "
+ + "control, not a security boundary. A future edit removed that language - restore it so nobody "
+ + "transfers SinkRenderAllowlist's security value onto this class by mistake.");
+ assertTrue(javadoc.contains("not a security boundary"),
+ () -> "PopulatorAllowlist's class Javadoc must keep stating that this allowlist is NOT a security "
+ + "boundary. A future edit removed that language - restore it so nobody transfers "
+ + "SinkRenderAllowlist's security value onto this class by mistake.");
+ }
+}
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/RegionDef_Test.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/RegionDef_Test.java
new file mode 100644
index 0000000000..ebdbbaf54e
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/RegionDef_Test.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.views;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * {@link RegionDef#validate()} matrix: id presence, populator-name allowlisting, and blank
+ * {@link RegionDef#allowedPopulators} entries.
+ */
+class RegionDef_Test extends TestBase {
+
+ @Test void a01_create_andFluentChain() {
+ var r = RegionDef.create("sidebar").populate("myWidget").allowPopulators("myWidget");
+ assertEquals("sidebar", r.id);
+ assertEquals("myWidget", r.populate);
+ assertEquals(java.util.Set.of("myWidget"), r.allowedPopulators);
+ }
+
+ @Test void a02_contractVersion_isOne() {
+ assertEquals("1", RegionDef.CONTRACT_VERSION);
+ }
+
+ @Test void b01_nullPopulate_accepted() {
+ RegionDef.create("sidebar").validate();
+ }
+
+ @Test void b02_defaultPopulate_accepted() {
+ RegionDef.create("sidebar").populate("default").validate();
+ }
+
+ @Test void b03_unallowlistedPopulate_rejected() {
+ var r = RegionDef.create("sidebar").populate("myWidget");
+ var e = assertThrows(IllegalArgumentException.class, r::validate);
+ assertTrue(e.getMessage().contains("myWidget"), e::getMessage);
+ }
+
+ @Test void b04_optedInPopulate_accepted() {
+ RegionDef.create("sidebar").populate("myWidget").allowPopulators("myWidget").validate();
+ }
+
+ @Test void c01_blankAllowedPopulatorsEntry_rejected() {
+ var r = RegionDef.create("sidebar").allowPopulators("myWidget", " ");
+ var e = assertThrows(IllegalArgumentException.class, r::validate);
+ assertTrue(e.getMessage().contains("allowPopulators"), e::getMessage);
+ }
+
+ @Test void d01_nullId_rejected() {
+ var r = RegionDef.create(null);
+ var e = assertThrows(IllegalArgumentException.class, r::validate);
+ assertTrue(e.getMessage().contains("id"), e::getMessage);
+ }
+
+ @Test void d02_blankId_rejected() {
+ var r = RegionDef.create(" ");
+ assertThrows(IllegalArgumentException.class, r::validate);
+ }
+}
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/RegionsHarness.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/RegionsHarness.java
new file mode 100644
index 0000000000..c23e46af7a
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/RegionsHarness.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.views;
+
+import static java.nio.charset.StandardCharsets.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.nio.file.*;
+import java.util.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.marshall.marshaller.*;
+
+/**
+ * Shared Node plumbing for the three {@code juneau-regions.js} behavioral harnesses.
+ *
+ * <p>
+ * The region runtime's behavioral coverage is split across three harness scripts by SUBJECT - the populate primitive,
+ * the message bus, the initial-broadcast barrier - and each one is driven by its own test class so a failure names the
+ * layer that broke. Splitting the driver plumbing out of those three classes is not just deduplication: it means the
+ * three run the SAME loader against the SAME three assets in the same order, so a "passes in one class, fails in
+ * another" result can only come from the harness under test.
+ *
+ * <p>
+ * Each harness is spawned once per JVM and its JSON report cached, because the barrier harness alone builds
+ * twenty-odd independent pages.
+ */
+final class RegionsHarness {
+
+ private static final Map<String,Map<?,?>> CACHE = new ConcurrentHashMap<>();
+ private static final Set<String> FAILED = ConcurrentHashMap.newKeySet();
+
+ private RegionsHarness() {}
+
+ /**
+ * Runs the named harness and returns its report, or null when this machine cannot run it (no Node, or the script
+ * is not on any of the paths a Maven or IDE working directory produces). Callers turn a null into a skip.
+ */
+ static Map<?,?> report(String harnessName) {
+ if (FAILED.contains(harnessName))
+ return null;
+ var cached = CACHE.get(harnessName);
+ if (cached != null)
+ return cached;
+ try {
+ if (!nodeAvailable())
+ return markUnavailable(harnessName);
+ var harness = locate(harnessName);
+ if (harness == null)
+ return markUnavailable(harnessName);
+ var report = Json.to(run(harness, harnessName), Map.class);
+ CACHE.put(harnessName, report);
+ return report;
+ } catch (Exception e) {
+ // A harness that cannot be READ is a skip; a harness that RAN and failed has already called fail().
+ throw new AssertionError("could not run " + harnessName + ": " + e, e);
+ }
+ }
+
+ private static Map<?,?> markUnavailable(String harnessName) {
+ FAILED.add(harnessName);
+ return null;
+ }
+
+ private static String run(Path harness, String harnessName) throws Exception {
+ var renders = Files.createTempFile("juneau-renders-", ".js");
+ var views = Files.createTempFile("juneau-views-", ".js");
+ var regions = Files.createTempFile("juneau-regions-", ".js");
+ var stdout = Files.createTempFile("regions-stdout-", ".json");
+ var stderr = Files.createTempFile("regions-stderr-", ".txt");
+ try {
+ Files.writeString(renders, asset(ViewsMixin.RENDERS_JS_RESOURCE), UTF_8);
+ Files.writeString(views, asset(ViewsMixin.VIEWS_JS_RESOURCE), UTF_8);
+ Files.writeString(regions, asset(ViewsMixin.REGIONS_JS_RESOURCE), UTF_8);
+ var p = new ProcessBuilder(List.of(
+ "node", harness.toString(), renders.toString(), views.toString(), regions.toString()))
+ .redirectOutput(stdout.toFile())
+ .redirectError(stderr.toFile())
+ .start();
+ if (!p.waitFor(60, TimeUnit.SECONDS)) {
+ p.destroyForcibly();
+ fail(harnessName + " did not finish within 60s; stderr:\n" + quietRead(stderr));
+ }
+ if (p.exitValue() != 0)
+ fail(harnessName + " exited " + p.exitValue() + "; stderr:\n" + quietRead(stderr)
+ + "\nstdout:\n" + quietRead(stdout));
+ return Files.readString(stdout, UTF_8);
+ } finally {
+ for (var f : List.of(renders, views, regions, stdout, stderr))
+ Files.deleteIfExists(f);
+ }
+ }
+
+ private static String asset(String resource) throws IOException {
+ try (var in = ViewsMixin.class.getResourceAsStream(resource)) {
+ assertNotNull(in, resource);
+ return new String(in.readAllBytes(), UTF_8);
+ }
+ }
+
+ private static boolean nodeAvailable() {
+ try {
+ var p = new ProcessBuilder("node", "--version").redirectErrorStream(true).start();
+ if (!p.waitFor(5, TimeUnit.SECONDS)) {
+ p.destroyForcibly();
+ return false;
+ }
+ return p.exitValue() == 0;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ private static Path locate(String harnessName) {
+ var basedir = System.getProperty("basedir");
+ if (basedir != null) {
+ var p = Path.of(basedir, "src/test/js/" + harnessName);
+ if (Files.isRegularFile(p))
+ return p.toAbsolutePath().normalize();
+ }
+ for (var rel : List.of(
+ "src/test/js/" + harnessName,
+ "juneau-rest/juneau-rest-server-views/src/test/js/" + harnessName
+ )) {
+ var p = Path.of(rel);
+ if (Files.isRegularFile(p))
+ return p.toAbsolutePath().normalize();
+ }
+ return null;
+ }
+
+ private static String quietRead(Path p) {
+ try { return Files.readString(p, UTF_8); }
+ catch (IOException e) { return "(unreadable: " + e.getMessage() + ")"; }
+ }
+
+ /** Reads the region runtime's source, for the source-shape assertions each driver opens with. */
+ static String regionsJs() throws IOException {
+ return asset(ViewsMixin.REGIONS_JS_RESOURCE);
+ }
+}
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/Regions_Barrier_Test.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/Regions_Barrier_Test.java
new file mode 100644
index 0000000000..3c2423d79a
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/Regions_Barrier_Test.java
@@ -0,0 +1,343 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.views;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assumptions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Always-on coverage for the page-scoped INITIAL-BROADCAST BARRIER and its bounded replay window.
+ *
+ * <p>
+ * The barrier exists for one shape: a control region that emits during its own {@code reason:"initial"} populate,
+ * before the regions that care have subscribed. Buffering that emit is the easy half. The hard half is RELEASING
+ * the buffer, and the failure the design rates worse than the bug it replaces is a barrier that never lifts - a page
+ * where all subsequent user-driven traffic is frozen for the life of the tab, with a clean console. That is why
+ * three separate non-settlement modes each have their own test, why the arming protocol has a loud backstop, and why
+ * several assertions here are against the CLOCK rather than against delivery order: for a hidden enrolled card,
+ * delivery is correct under both the right and the wrong reading and only the latency differs, so an order-based
+ * assertion passes either way.
+ *
+ * <p>
+ * Behavioral half: {@code src/test/js/regions-barrier.cjs}.
+ */
+class Regions_Barrier_Test extends TestBase {
+
+ private static Map<?,?> report() {
+ var r = RegionsHarness.report("regions-barrier.cjs");
+ assumeTrue(r != null, "node not available or regions-barrier.cjs not found - behavioral layer skipped");
+ return r;
+ }
+
+ private static void assertAllTrue(Map<?,?> r, String... keys) {
+ for (var k : keys)
+ assertEquals(true, r.get(k), () -> k + " -> " + r.get(k) + " in " + r);
+ }
+
+ private static void assertNothingReceived(Map<?,?> r, String key) {
+ assertEquals(List.of(), r.get(key), () -> key + " must have received nothing, got " + r.get(key));
+ }
+
+ /** Every reading in {@code key} is the given clock time - the assertion that catches a needless page-wide wait. */
+ private static void assertDeliveredAt(Map<?,?> r, String key, int expectedMs) {
+ var readings = (List<?>)r.get(key);
+ assertFalse(readings.isEmpty(), () -> key + " recorded no delivery at all");
+ for (var at : readings)
+ assertEquals(expectedMs, ((Number)at).intValue(),
+ () -> key + " delivered at " + readings + ", expected every delivery at " + expectedMs + "ms");
+ }
+
+ // =================================================================================================================
+ // 49 - the case the barrier exists for.
+ // =================================================================================================================
+
+ /**
+ * A control region that emits during its own {@code reason:"initial"} populate AND INITIALIZES FIRST still reaches
+ * three chart regions that subscribe during their own later initial populates - including one whose populate
+ * returns a Promise settling after the control's, which is what proves the barrier waits on ASYNC populates and
+ * not merely on synchronous return. A region activated LATER receives nothing retroactively: the design declines
+ * replay-on-subscribe for a region that was never a barrier member.
+ */
+ @Test void b49_aControlEmittingDuringItsOwnInitialPopulateReachesLaterSubscribers() {
+ var r = report();
+ assertEquals(List.of(List.of(), List.of(), List.of()), r.get("t49a_beforeAsyncSettle"),
+ "nothing may be delivered while an initial populate is still in flight");
+ assertEquals(List.of(List.of("M1"), List.of("M1"), List.of("M1")), r.get("t49a_afterAsyncSettle"), r::toString);
+ assertDeliveredAt(r, "t49a_drainedAtZeroNotDeadline", 0);
+ assertAllTrue(r, "t49a_noDeadlineWarning", "t49a_controlDidNotReceiveItsOwn");
+ assertNothingReceived(r, "t49b_lateActivatedGetsNothing");
+ }
+
+ // =================================================================================================================
+ // 49a / 49b / 49c - the three non-settlement modes, each with its own test.
+ // =================================================================================================================
+
+ /** Mode 1: a SYNCHRONOUSLY-THROWING initial populate releases the barrier. */
+ @Test void b49a_aSynchronousThrowReleasesTheBarrier() {
+ var r = report();
+ assertEquals(List.of(List.of(), List.of()), r.get("t49a1_heldUntilThirdSettles"), r::toString);
+ assertEquals(List.of(List.of("M1"), List.of("M1")), r.get("t49a1_drainedOnThirdSettle"), r::toString);
+ assertDeliveredAt(r, "t49a1_drainedAtZero", 0);
+ assertEquals("error", r.get("t49a1_throwerErrored"));
+ assertEquals(true, r.get("t49a1_noDeadlineNeeded"), "a throw must not make the page wait for the deadline");
+ }
+
+ /**
+ * Mode 2: a REJECTED initial populate releases the barrier. Worth a separate test from the synchronous throw
+ * because the natural implementation - a {@code try} around the call - catches the throw and NOT the rejection,
+ * and the symptom of missing the second is a page-wide freeze with a green-looking console.
+ */
+ @Test void b49b_aRejectedPromiseReleasesTheBarrier() {
+ var r = report();
+ assertNothingReceived(r, "t49b_heldWhileRejectionPending");
+ assertEquals(List.of("M1"), r.get("t49b_drainedOnRejection"), r::toString);
+ assertDeliveredAt(r, "t49b_drainedAtZero", 0);
+ assertEquals("error", r.get("t49b_rejectorErrored"));
+ assertEquals(true, r.get("t49b_noDeadlineNeeded"), r::toString);
+ }
+
+ /**
+ * Mode 3: a NEVER-SETTLING initial populate releases the barrier AT THE DEADLINE, with a warning naming the
+ * region. The barrier is an ORDERING device and not a lifecycle one, so the hung populate is not aborted and its
+ * container is left exactly as it left it; and traffic AFTER the release flows normally rather than staying frozen
+ * for the life of the page.
+ */
+ @Test void b49c_aNeverSettlingPopulateReleasesTheBarrierAtTheDeadline() {
+ var r = report();
+ assertNothingReceived(r, "t49c_heldBeforeDeadline");
+ assertNothingReceived(r, "t49c_stillHeldAt1999");
+ assertEquals(List.of("M1"), r.get("t49c_drainedAtDeadline"), r::toString);
+ assertDeliveredAt(r, "t49c_deliveredAt", 2000);
+ assertAllTrue(r, "t49c_warningNamesRegion", "t49c_oneWarningForThePage");
+ assertAllTrue(r, "t49c_hungNotAborted", "t49c_hungContainerUntouched");
+ assertEquals("loading", r.get("t49c_hungStillLoading"));
+ assertEquals(List.of("M1", "M2"), r.get("t49c_laterTrafficFlows"),
+ "traffic after the release must flow, not stay frozen for the life of the page");
+ }
+
+ /**
+ * The deadline is a CLAMPED CONSTANT: not settable through the published namespace, a global, or an attribute on
+ * the region or its host. An author-settable barrier deadline is a way to configure a page-wide freeze.
+ */
+ @Test void b49d_theDeadlineIsNotConfigurable() {
+ var r = report();
+ assertEquals(2000, ((Number)r.get("t49d_value")).intValue());
+ assertNothingReceived(r, "t49d_notReleasedAtFive");
+ assertEquals(List.of("M1"), r.get("t49d_releasedAtTwoThousand"), r::toString);
+ assertDeliveredAt(r, "t49d_deliveredAt", 2000);
+ }
+
+ // =================================================================================================================
+ // 49e / 49f - page scope and the arming protocol.
+ // =================================================================================================================
+
+ /**
+ * THE TWO-HOST CASE. A page carrying both a card grid and a tabbed page shell, whose two runtimes boot
+ * independently and hold no reference to each other: the barrier is PAGE-scoped, so the grid's control card still
+ * reaches a chart in the page shell's initial panel. Under a host-scoped barrier this fails, and it fails
+ * INTERMITTENTLY - on boot order - which is the worst way for it to fail. Late activation gets nothing whether
+ * the late region is a naked tab body or a card in a non-initial panel, so that rule is about activation and not
+ * about which runtime enrolled the region.
+ */
+ @Test void b49e_theBarrierIsPageScopedAcrossTwoIndependentRuntimes() {
+ var r = report();
+ assertNothingReceived(r, "t49e_notDrainedAfterFirstRuntime");
+ assertEquals(List.of("M1"), r.get("t49e_i_gridChart"), r::toString);
+ assertEquals(List.of("M1"), r.get("t49e_i_panelChart"),
+ "a page-scoped barrier reaches the other runtime's regions");
+ assertDeliveredAt(r, "t49e_i_drainedAtZero", 0);
+ assertNothingReceived(r, "t49e_ii_lazyTab");
+ assertNothingReceived(r, "t49e_iii_lazyCard");
+ }
+
+ /**
+ * THE ARMING PROTOCOL. (i) An EMPTY outstanding set is not enough - the barrier holds until every registered
+ * runtime has declared its enrolment walk complete, because "no regions outstanding" and "no regions enrolled
+ * yet" are indistinguishable from the barrier's side. (ii) A page with NO regions pays nothing: nothing arms, no
+ * timer is scheduled, no document listener is added. (iii) A registered runtime that NEVER reports has the
+ * barrier lift one macrotask after DOMContentLoaded with exactly one error naming it - it degrades LOUDLY, which
+ * is the inverse of the failure mode a clock-only design was rejected for.
+ */
+ @Test void b49f_theArmingProtocolHoldsOnAnEmptySetAndHasALoudBackstop() {
+ var r = report();
+ assertNothingReceived(r, "t49f_i_heldOnEmptyOutstanding");
+ assertEquals(List.of("M1"), r.get("t49f_i_liftedOnSecondReport"), r::toString);
+ assertDeliveredAt(r, "t49f_i_liftedAtZero", 0);
+
+ assertAllTrue(r, "t49f_ii_noTimers", "t49f_ii_noDocumentListeners", "t49f_ii_assetLoadedButUnarmed");
+
+ assertNothingReceived(r, "t49f_iii_heldBeforeMacrotask");
+ assertEquals(List.of("M1"), r.get("t49f_iii_liftedAtMacrotask"), r::toString);
+ assertDeliveredAt(r, "t49f_iii_liftedAtZeroNotDeadline", 0);
+ assertAllTrue(r, "t49f_iii_oneErrorNamingRuntime", "t49f_iii_errorMentionsReady");
+ }
+
+ // =================================================================================================================
+ // 49g / 49h / 49i / 49l - the released-region replay window.
+ // =================================================================================================================
+
+ /**
+ * THE REPLAY WINDOW DELIVERS. The healthy regions receive the broadcast at the deadline - no page-wide wait -
+ * and the slow region's LATER {@code ctx.on} receives the same messages, in the same order, exactly once. The
+ * assertion that distinguishes a bounded replay window from a retained cache is the last one: a subsequent LIVE
+ * emit arrives after the replayed ones and is not duplicated.
+ */
+ @Test void b49g_aReleasedRegionsLaterSubscribeReceivesWhatItMissed() {
+ var r = report();
+ assertNothingReceived(r, "t49g_heldBeforeDeadline");
+ assertEquals(List.of("M1"), r.get("t49g_healthyGotItAtDeadline"), r::toString);
+ assertDeliveredAt(r, "t49g_healthyAt", 2000);
+ assertNothingReceived(r, "t49g_slowNothingYet");
+ assertEquals(List.of("M1"), r.get("t49g_slowReplayed"), r::toString);
+ assertEquals(true, r.get("t49g_slowReplayedOnce"), r::toString);
+ assertEquals(List.of("M1", "M2"), r.get("t49g_slowAfterLiveEmit"),
+ "a live emit lands after the replayed ones and is not duplicated");
+ assertEquals(List.of("M1", "M2"), r.get("t49g_healthyAfterLiveEmit"), r::toString);
+ }
+
+ /**
+ * THE WINDOW CLOSES, AND IS NOT A RETAINED CACHE. Four negatives, because each is a way the replay could grow
+ * into the retained-cache option the design rejected: a region enrolled after the lift gets nothing; a released
+ * region that subscribes after its OWN populate settles gets nothing; the buffer is discarded once the last
+ * window closes; and the replay reads NO payload property - which is what keeps payload opacity true on the
+ * second delivery path, and precisely the objection the retained-cache option failed.
+ */
+ @Test void b49h_theReplayWindowClosesAndIsNotARetainedCache() {
+ var r = report();
+ assertNothingReceived(r, "t49h_i_lateEnrolledGetsNothing");
+ assertNothingReceived(r, "t49h_ii_afterOwnSettleGetsNothing");
+ assertNothingReceived(r, "t49h_iii_freshRegionGetsNothing");
+ assertAllTrue(r, "t49h_iv_replayReadsNothing", "t49h_iv_replayedIdentityPreserved");
+ assertEquals(List.of("M1"), r.get("t49h_iv_replayed"), r::toString);
+ }
+
+ /**
+ * A REPLAY IS A FAN-OUT: a replayed handler that emits has its emission QUEUED like any other, and a second
+ * released region whose window is still open receives it. Run as two arms of the same page - one where both
+ * regions are deadline-released and one where both settle before the deadline - and the expected sequences are
+ * DERIVED by requiring the two arms to agree, rather than asserted as literals. That is what keeps the replay
+ * path from drifting out of agreement with ordinary delivery: a literal would let both drift together.
+ */
+ @Test void b49i_replayIsAFanOutAndAgreesWithOrdinaryDelivery() {
+ var r = report();
+ assertEquals(List.of("A-had-M2-at-emit-return:false"), r.get("t49i_i_emitWasQueuedNotLive"),
+ "an emit from a replayed handler must be queued, not delivered live inside the replay");
+ assertEquals(List.of("M1", "M2"), r.get("t49i_ii_cReceivedBoth"),
+ "a still-open second window receives what the replay emits");
+ assertEquals(true, r.get("t49i_iii_identicalSequences"),
+ () -> "released arm " + r.get("t49i_iii_released") + " != control arm " + r.get("t49i_iii_control"));
+ assertNothingReceived(r, "t49i_iv_lateEnrolled");
+ assertNothingReceived(r, "t49i_iv_hiddenAtBoot");
+ assertEquals(true, r.get("t49i_noCycleError"), r::toString);
+ }
+
+ /**
+ * THE LAST-WINDOW CLOSE, with a genuinely SYNCHRONOUS settle - a thenable whose {@code then} runs the framework's
+ * continuation in the caller's own turn, so a released region's close condition becomes true PARTWAY THROUGH a
+ * live fan-out. Resolving an ordinary promise from inside a subscriber cannot reach this case at all: resolving
+ * queues a microtask while the fan-out is synchronous, so the continuation runs after the drain has finished and
+ * the assertion silently tests the ordinary case instead.
+ *
+ * <p>
+ * Membership is snapshotted at the fan-out's START, so the message IS in the buffer that region replays - had the
+ * close taken effect immediately it would have replayed nothing - and the buffer's contents depend on message
+ * order and member-open-time rather than on the order subscribers happen to run in, which is what the two
+ * subscription-order arms check.
+ */
+ @Test void b49l_aWindowThatClosesMidFanOutStillCountedForThatFanOut() {
+ var r = report();
+ assertEquals(List.of("M3"), r.get("t49l_i_cReplayedM3"), r::toString);
+ assertEquals(true, r.get("t49l_i_exactlyOnce"), r::toString);
+ assertEquals(true, r.get("t49l_ii_orderIndependent"),
+ () -> "the two subscription-order arms disagreed: " + r.get("t49l_ii_arms"));
+ assertEquals(List.of("M3", "M4"), r.get("t49l_iii_cGotM4Live"), r::toString);
+ assertEquals(true, r.get("t49l_iv_bufferDiscarded"),
+ "the buffer must be discarded once the last window closes");
+ }
+
+ // =================================================================================================================
+ // 49j / 49k - membership axes and the two independent caps.
+ // =================================================================================================================
+
+ /**
+ * ENROLLED-BUT-HIDDEN IS NOT A BARRIER MEMBER: enrolment and barrier membership are different axes, and getting
+ * them confused means every tabbed card page waits the full deadline before the visible card gets its initial
+ * broadcasts. The first assertion is the one with teeth and it is against the CLOCK, because delivery is correct
+ * under both readings and only the latency differs. The hidden region IS enrolled - it has a handle and a
+ * reachable teardown - it has simply had no populate at all, which is WHY it is not outstanding rather than a
+ * special case; and activating it later produces exactly one {@code reason:"activate"} populate that receives
+ * nothing retroactively.
+ */
+ @Test void b49j_anEnrolledButHiddenRegionIsNotABarrierMember() {
+ var r = report();
+ assertDeliveredAt(r, "t49j_i_deliveredAt", 0);
+ assertEquals(List.of("M1"), r.get("t49j_i_visibleGotIt"), r::toString);
+ assertEquals(0, ((Number)r.get("t49j_i_clockAtDelivery")).intValue(),
+ "a hidden card must not delay the drain to the deadline");
+ assertEquals(true, r.get("t49j_i_noDeadlineWarning"), r::toString);
+
+ assertAllTrue(r, "t49j_ii_enrolled", "t49j_ii_teardownReachable");
+ assertEquals("hiddenCard", r.get("t49j_ii_hasKey"));
+ assertEquals("idle", r.get("t49j_iii_state"));
+ assertEquals(true, r.get("t49j_iii_noContent"), r::toString);
+
+ assertEquals(List.of("activate"), r.get("t49j_iv_reasons"), r::toString);
+ assertNothingReceived(r, "t49j_iv_receivedNothing");
+ assertEquals(true, r.get("t49j_iv_activateOnce"), r::toString);
+ }
+
+ /**
+ * THE REPLAY BUFFER'S OVERFLOW IS NOT A CYCLE ERROR. 257 legitimate, non-cyclic, user-driven broadcasts, each
+ * from a different emitter at the top of its own turn, so no fan-out is ever nested.
+ *
+ * <p>
+ * (i) No cycle error, at any point. Under a single shared cap this page raises one at append 65 - a false
+ * diagnostic naming a defect that does not exist, which is worse than silence. (ii) A region that settles at
+ * append 200 replays exactly the first 200 in order, exactly once, so two regions on the same page reach two
+ * different outcomes. (iii) At append 257 the buffer is ABANDONED WHOLE and loudly: the still-open window
+ * receives NO replay rather than a truncated one, because a run of messages with a hole is the outcome the design
+ * rates worse than none - the region cannot detect it - and exactly one error names the affected key, the cap, the
+ * exactly-once failure, and the fact that this is not a cycle. (iv) The two caps are independent in BOTH
+ * directions: this page overflowed its buffer without a cycle error, and a genuinely nesting page raises the cycle
+ * error without overflowing a buffer. Under one shared constant one of those two must fail.
+ */
+ @Test void b49k_theTwoCapsAreIndependentAndOverflowIsNotReportedAsACycle() {
+ var r = report();
+ assertEquals(true, r.get("t49k_bothReleased"), r::toString);
+
+ assertEquals(true, r.get("t49k_i_noCycleError"),
+ "257 top-of-turn broadcasts contain no cycle and must not be reported as one");
+
+ assertEquals(200, ((Number)r.get("t49k_ii_dReplayCount")).intValue(), r::toString);
+ assertEquals("m1", r.get("t49k_ii_dReplayFirst"));
+ assertEquals("m200", r.get("t49k_ii_dReplayLast"));
+ assertEquals(true, r.get("t49k_ii_dInOrder"), r::toString);
+
+ assertAllTrue(r, "t49k_iii_exactlyOneOverflowError", "t49k_iii_namesKey", "t49k_iii_namesCap",
+ "t49k_iii_namesExactlyOnce", "t49k_iii_saysNotACycle", "t49k_iii_namesOnlyTheOpenWindow",
+ "t49k_iii_noPartialReplay");
+ assertNothingReceived(r, "t49k_iii_cReceivedNoReplay");
+
+ assertAllTrue(r, "t49k_iv_bufferOverflowedWithoutCycleError", "t49k_iv_capsDiffer",
+ "t49k_iv_nestingRaisesCycleError", "t49k_iv_nestingRaisedNoOverflowError");
+ }
+}
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/Regions_Bus_Test.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/Regions_Bus_Test.java
new file mode 100644
index 0000000000..5a5adf476b
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/Regions_Bus_Test.java
@@ -0,0 +1,283 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.views;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assumptions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Always-on coverage for the region MESSAGE BUS in {@code juneau-regions.js}: broadcast by default, opt-in targeted
+ * delivery, opaque payloads, per-subscriber error isolation, subscription-ordered synchronous delivery with
+ * emit-during-delivery queued rather than nested, unsubscribe on teardown, the closed set of framework-emitted
+ * schemas, and both directions of SD-2's emit-ownership invariant.
+ *
+ * <p>
+ * The behavioral half runs the real runtime source under a DOM shim (see {@code src/test/js/regions-bus.cjs}).
+ */
+class Regions_Bus_Test extends TestBase {
+
+ private static Map<?,?> report() {
+ var r = RegionsHarness.report("regions-bus.cjs");
+ assumeTrue(r != null, "node not available or regions-bus.cjs not found - behavioral layer skipped");
+ return r;
+ }
+
+ private static void assertAllTrue(Map<?,?> r, String... keys) {
+ for (var k : keys)
+ assertEquals(true, r.get(k), () -> k + " -> " + r.get(k) + " in " + r);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List<String> list(Map<?,?> r, String key) {
+ return (List<String>)r.get(key);
+ }
+
+ // =================================================================================================================
+ // Source shape.
+ // =================================================================================================================
+
+ /**
+ * SD-2, THE EMIT-OWNERSHIP INVARIANT, as a source scan: the region runtime installs NO delegated listener over
+ * author-painted content. Chrome auto-emits because the framework owns the chrome; content emits explicitly
+ * because the author owns the content. A "helpful" click delegate over a region container is exactly how this
+ * invariant gets broken later, and it would break it silently - so the negative is pinned here as well as
+ * behaviorally.
+ */
+ @Test void a01_sourceShape_theRuntimeBindsNoDelegateOverAuthorContent() throws Exception {
+ var body = RegionsHarness.regionsJs();
+ assertFalse(body.contains("addEventListener(\"click\""),
+ "the region runtime must not bind a click listener - content emits explicitly (SD-2)");
+ assertFalse(body.contains("addEventListener(\"change\""), body);
+ assertFalse(body.contains("addEventListener(\"input\""), body);
+ assertFalse(body.contains("addEventListener(\"submit\""), body);
+ // The whole listener budget is TWO, and neither is bound on a region's contents: the parent->child abort
+ // linkage is on an AbortSignal, and the barrier's backstop is on the document. Pinning the COUNT is what
+ // makes a third listener - which is how a content delegate would arrive - fail here rather than pass because
+ // it used an event name this test did not think to name above.
+ assertEquals(2, body.split("addEventListener\\(", -1).length - 1,
+ "the region runtime's only listeners are the parent->child abort linkage and the barrier backstop");
+ assertTrue(body.contains("parent.addEventListener(\"abort\""), body);
+ assertTrue(body.contains("window.document.addEventListener(\"DOMContentLoaded\""), body);
+ }
+
+ /**
+ * Payload OPACITY as a source scan, to complement the behavioral trap: the framework neither clones, serializes
+ * nor routes on the payload, so a payload carrying a DOM node or a class instance survives delivery unchanged and
+ * the payload's shape is a contract between the emitting and receiving regions rather than with the framework.
+ */
+ @Test void a02_sourceShape_thePayloadIsNeverClonedOrInspected() throws Exception {
+ var body = RegionsHarness.regionsJs();
+ for (var forbidden : List.of("structuredClone", "JSON.parse(JSON.stringify", "Object.assign({}, msg",
+ "msg.kind ===", "message.kind ==="))
+ assertFalse(body.contains(forbidden), "the bus must not " + forbidden + " a payload");
+ }
+
+ // =================================================================================================================
+ // Delivery.
+ // =================================================================================================================
+
+ /**
+ * A broadcast reaches every subscriber EXCEPT the emitter, so a self-reflecting control cannot learn its own state
+ * from the bus and has to keep it in local state - which is the intended discipline, not an oversight.
+ */
+ @Test void b17_broadcastReachesEveryoneButTheEmitter() {
+ var r = report();
+ assertEquals(List.of(), r.get("t17_a"), "the emitter must not receive its own broadcast");
+ assertEquals(List.of("ping"), r.get("t17_b"), r::toString);
+ assertEquals(List.of("ping"), r.get("t17_c"), r::toString);
+ assertEquals("a", r.get("t17_metaFrom"));
+ assertAllTrue(r, "t17_metaToNullOnBroadcast", "t17_synchronous");
+ }
+
+ /**
+ * Targeted delivery reaches EXACTLY one region, resolving either a short id within the emitter's own host or a
+ * fully-qualified {@code host/id} key; an unknown target is a no-op plus a warning rather than a throw, because
+ * one region's stale target must not take down the emitting region's populate.
+ */
+ @Test void b18_targetedDeliveryResolvesShortIdsAndQualifiedKeys() {
+ var r = report();
+ assertEquals(List.of("short"), r.get("t18_shortB"), r::toString);
+ assertEquals(List.of(), r.get("t18_shortC"), "a targeted emit must not broadcast");
+ assertEquals("b", r.get("t18_metaTo"));
+ assertEquals(List.of("qualified"), r.get("t18_qualifiedC"), r::toString);
+ assertEquals(List.of("short"), r.get("t18_qualifiedB"), "the qualified target must not reach b");
+ assertEquals(List.of("grid/a", "grid/b", "grid/c"), r.get("t18_keys"), r::toString);
+ }
+
+ /** An unknown - or self-addressed - target warns and delivers nothing, and never throws. */
+ @Test void b19_unknownAndSelfAddressedTargetsWarnWithoutThrowing() {
+ var r = report();
+ assertAllTrue(r, "t19_noThrow", "t19_warned", "t19_nobodyGotIt",
+ "t19_selfAddressWarned", "t19_selfAddressNotDelivered");
+ }
+
+ /**
+ * PAYLOAD OPACITY, behaviorally: a getter-trapped payload arrives having recorded ZERO property reads, on both
+ * the broadcast and the targeted path, and every subscriber receives the SAME OBJECT rather than a copy.
+ */
+ @Test void b20_payloadArrivesUnreadAndUnclonedS() {
+ var r = report();
+ assertEquals(List.of(), r.get("t20_reads"), () -> "the framework read: " + r.get("t20_reads"));
+ assertAllTrue(r, "t20_zeroReads", "t20_zeroReadsTargeted", "t20_identityShared");
+ }
+
+ /**
+ * A throwing subscriber does not block the rest of the fan-out and IS reported. It is deliberately NOT
+ * auto-unsubscribed after repeated throws: silently dropping a subscriber turns a loud bug into a mysterious one,
+ * so it keeps receiving and keeps reporting.
+ */
+ @Test void b21_aThrowingSubscriberIsIsolatedReportedAndNotDropped() {
+ var r = report();
+ assertEquals(List.of("m1"), r.get("t21_cStillGotIt"), r::toString);
+ assertEquals(List.of("m1", "m2"), r.get("t21_stillSubscribed"), "a thrower must not be auto-unsubscribed");
+ assertEquals(List.of("m1", "m2"), r.get("t21_cGotBoth"), r::toString);
+ assertAllTrue(r, "t21_reported", "t21_reportedTwice");
+ }
+
+ /** Delivery order is SUBSCRIPTION order. */
+ @Test void b22_deliveryOrderIsSubscriptionOrder() {
+ var r = report();
+ assertEquals(List.of("z", "m", "a"), r.get("t22_order"), r::toString);
+ }
+
+ /**
+ * An emit DURING delivery is QUEUED, not nested - asserted by CALL-ORDER RECORDING rather than by
+ * absence-of-crash, because a nested implementation does not crash: it just re-enters a subscriber mid-paint, and
+ * the only visible difference is the order the handlers ran in. The nested signature would interleave the second
+ * message's handlers inside the first's; the queued signature completes the first fan-out entirely first.
+ */
+ @Test void b23_emitDuringDeliveryIsQueuedNotNested() {
+ var r = report();
+ assertEquals(List.of("enter:b:m1", "exit:b:m1", "enter:c:m1", "exit:c:m1",
+ "enter:c:m2", "exit:c:m2", "enter:driver:m2", "exit:driver:m2"), r.get("t23_trace"), r::toString);
+ assertAllTrue(r, "t23_notNested", "t23_m1FanOutCompletedFirst");
+ assertEquals(List.of("m1", "m2"), r.get("t23_cGot"), r::toString);
+ }
+
+ /**
+ * An emit cycle terminates the drain and errors loudly, PRINTING THE CYCLE - the shortest repeating
+ * {@code from -> to} suffix - rather than an arbitrary pair of keys. Asserted with a THREE-region ring as well
+ * as a two-region one, because the two-region case cannot tell "prints the cycle" apart from "prints the last two
+ * hops", which is the mistake worth catching: a wrong cycle print sends the reader to the wrong region.
+ */
+ @Test void b24_anEmitCycleIsTerminatedAndPrintsTheActualCycle() {
+ var r = report();
+ assertAllTrue(r, "t24_2_exactlyOneError", "t24_2_terminated", "t24_2_namesEveryRegion",
+ "t24_3_exactlyOneError", "t24_3_terminated", "t24_3_namesEveryRegion");
+ assertEquals(2, ((Number)r.get("t24_2_hopCount")).intValue(), r::toString);
+ assertEquals(3, ((Number)r.get("t24_3_hopCount")).intValue(),
+ "a three-region cycle must print three hops, not the last two");
+ }
+
+ /**
+ * THE LEAK TEST: a torn-down region receives nothing even though its handler is still reachable from the test's
+ * own closure - so the unsubscribe is real and not merely a dropped reference. {@code ctx.on}'s returned
+ * unsubscribe works independently, leaving the region itself live and healthy.
+ */
+ @Test void b25_teardownUnsubscribesAndTheReturnedUnsubscribeIsIndependent() {
+ var r = report();
+ assertEquals(List.of("before"), r.get("t25_beforeTeardown"), r::toString);
+ assertEquals(List.of("before"), r.get("t25_afterTeardown"), "a torn-down region must receive nothing");
+ assertEquals(List.of("before", "after"), r.get("t25_siblingUnaffected"), r::toString);
+ assertEquals(List.of("one"), r.get("t26_receivedBeforeOff"), r::toString);
+ assertEquals("ok", r.get("t26_stateStillOk"));
+ assertEquals(true, r.get("t26_regionStillLive"), r::toString);
+ }
+
+ /**
+ * Teardown step 4: a message the torn-down region had already QUEUED, but which has not been delivered yet, is
+ * DISCARDED. A region that is going away should not still be driving the page after it is gone.
+ */
+ @Test void b25a_aQueuedMessageFromATornDownRegionIsDiscarded() {
+ var r = report();
+ assertEquals(List.of("m1"), r.get("t_step4_cGot"), r::toString);
+ assertEquals(true, r.get("t_step4_queuedMessageDropped"), r::toString);
+ }
+
+ /**
+ * The framework-emitted set is SMALL AND CLOSED - selection changed, detail toggled, table redrew - every kind is
+ * namespaced, and 27a/27c's chrome half of SD-2 holds: a region beside the chrome is driven with NO author emit
+ * code at all, only a {@code ctx.on} plus a {@code msg.viewId} filter.
+ */
+ @Test void b27_theFrameworkEmittedSetIsClosedAndChromeAutoEmits() {
+ var r = report();
+ assertEquals(List.of("juneau:selection-changed", "juneau:detail-toggled", "juneau:table-redrew"),
+ r.get("t27_kinds"), r::toString);
+ assertEquals("juneau:framework", r.get("t27_senderIsFramework"));
+ assertEquals(true, r.get("t27_allNamespaced"), r::toString);
+ assertEquals(List.of("juneau:selection-changed", "juneau:detail-toggled", "juneau:table-redrew"),
+ r.get("t27a_drivenWithNoAuthorEmit"), "chrome auto-emits: the consumer wrote no emit call");
+ }
+
+ /**
+ * A GOLDEN per framework-emitted message - kind, {@code schemaVersion} and the whole field set - plus the
+ * two-tables-on-one-page disambiguation case. {@code viewId} on the payload is what makes that case answerable
+ * at all: {@code meta.from} names the framework rather than a table, and a framework broadcast has no host for a
+ * short target to resolve against, so without {@code viewId} a subscriber cannot tell WHICH table's selection
+ * changed.
+ */
+ @Test void b51_frameworkMessageSchemasAreGoldenAndCarryViewId() {
+ var r = report();
+ assertEquals(List.of("added", "ids", "kind", "removed", "rows", "schemaVersion", "viewId"),
+ r.get("t51_selectionKeys"), r::toString);
+ assertEquals(List.of("expanded", "generation", "kind", "rowId", "schemaVersion", "viewId"),
+ r.get("t51_detailKeys"), r::toString);
+ assertEquals(List.of("kind", "nested", "page", "rowCount", "schemaVersion", "viewId"),
+ r.get("t51_redrewKeys"), r::toString);
+ assertEquals(true, r.get("t51_schemaVersionsIndependentOfContract"),
+ "a message schema version is its own, not the region contract version");
+ assertEquals(true, r.get("t51_filteredByViewId"), r::toString);
+ assertEquals(List.of("gacks", "other"), r.get("t51_viewIdsSeen"), r::toString);
+ }
+
+ /**
+ * SD-2's NEGATIVE HALF: CONTENT DOES NOT AUTO-EMIT. A populate paints three clickable elements with its own
+ * listeners, calls no emit, and clicking them produces ZERO bus traffic - and the framework has bound no listener
+ * of its own beside the author's, on the buttons or on the region container. This is the test that stops someone
+ * later adding a convenience click delegate over author DOM, which would break the invariant in a way no positive
+ * test notices.
+ */
+ @Test void b27b_contentDoesNotAutoEmit() {
+ var r = report();
+ assertAllTrue(r, "t27b_authorHandlersRan", "t27b_zeroBusTraffic",
+ "t27b_containerHasNoFrameworkListener", "t27b_stillZeroAfterContainerClick");
+ assertEquals(List.of("click:1", "click:1", "click:1"), r.get("t27b_listenerCounts"),
+ "each painted node must carry the author's listener and no framework listener");
+ }
+
+ /**
+ * OUT-OF-ORDER SETTLE, the flagship shape for update-in-place regions. Three rapid messages with responses
+ * returning in the order 3, 1, 2: the container holds the LAST REQUESTED probe, every superseded fetch's signal
+ * is aborted, a superseded response can no longer settle at all, and no {@code AbortError} ever surfaces as a
+ * region error - the swallow the design points at as the established idiom.
+ */
+ @Test void b50_outOfOrderSettleLeavesTheLastRequestedResponsePainted() {
+ var r = report();
+ assertAllTrue(r, "t50_twoFetches", "t50_firstSignalAborted", "t50_secondSignalLive",
+ "t50_secondSettled", "t50_staleFirstCannotSettle", "t50_noRegionError");
+ assertEquals("probe-2", r.get("t50_containerHoldsSecond"), "a stale response must not overwrite a fresh one");
+ assertEquals("probe-5", r.get("t50a_holdsLast"), r::toString);
+ assertEquals(List.of(true, true, false), r.get("t50a_earlierSignalsAborted"), r::toString);
+ assertEquals(List.of(false, false), r.get("t50a_supersededCannotSettle"), r::toString);
+ assertEquals(true, r.get("t50a_noAbortErrorSurfaced"), r::toString);
+ }
+}
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/Regions_Primitive_Test.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/Regions_Primitive_Test.java
new file mode 100644
index 0000000000..b4b158016f
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/Regions_Primitive_Test.java
@@ -0,0 +1,334 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.views;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assumptions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Always-on coverage for the {@code populate(ctx, container)} primitive in {@code juneau-regions.js}.
+ *
+ * <p>
+ * The primitive is the one member of the region contract that every populator sees on every invocation, so its
+ * surface is pinned as a GOLDEN rather than spot-checked: the whole {@code ctx} key set, the whole {@code declared}
+ * key set and the whole {@code ids} key set are compared against literals, and a member that changes shape breaks
+ * this test rather than surfacing later as an author-visible contract break. The behavioral half runs the real
+ * runtime source under a DOM shim and a controllable clock (see {@code src/test/js/regions-primitive.cjs}).
+ */
+class Regions_Primitive_Test extends TestBase {
+
+ private static Map<?,?> report() {
+ var r = RegionsHarness.report("regions-primitive.cjs");
+ assumeTrue(r != null, "node not available or regions-primitive.cjs not found - behavioral layer skipped");
+ return r;
+ }
+
+ private static void assertAllTrue(Map<?,?> r, String... keys) {
+ for (var k : keys)
+ assertEquals(true, r.get(k), () -> k + " -> " + r.get(k) + " in " + r);
+ }
+
+ // =================================================================================================================
+ // Source shape.
+ // =================================================================================================================
+
+ /**
+ * The AbortController baseline is tested EXACTLY ONCE, at load. A per-call feature test is what makes a degraded
+ * mode possible, and the design's answer to a missing AbortController is a refusal rather than a degraded mode.
+ */
+ @Test void a01_sourceShape_abortControllerTestedOnceAtLoad() throws Exception {
+ var body = RegionsHarness.regionsJs();
+ var occurrences = body.split("typeof AbortController", -1).length - 1;
+ assertEquals(1, occurrences, "the AbortController baseline must be feature-tested exactly once, at load");
+ assertTrue(body.contains("const HAS_ABORT_CONTROLLER"), body);
+ }
+
+ /**
+ * {@code fork} is attached with {@code Object.defineProperty} on the INSTANCE and the region runtime never touches
+ * {@code AbortSignal.prototype}. The lazy way to implement {@code fork()} is a prototype patch, which mutates a
+ * platform type for every script on the page including ones that have never heard of Juneau.
+ */
+ @Test void a02_sourceShape_forkIsAnInstancePropertyNeverAPrototypePatch() throws Exception {
+ var body = RegionsHarness.regionsJs();
+ assertTrue(body.contains("Object.defineProperty(signal, \"fork\""), body);
+ // The prose above the attachment says "NEVER on AbortSignal.prototype", so the scan is for a MEMBER ACCESS on
+ // that prototype rather than for the phrase.
+ assertFalse(body.contains("AbortSignal.prototype."), "the region runtime must never patch AbortSignal.prototype");
+ assertFalse(body.contains("AbortSignal.prototype["), "the region runtime must never patch AbortSignal.prototype");
+ }
+
+ /**
+ * The barrier deadline is a clamped constant with no setter and no configuration read: it is never resolved from
+ * an attribute, a global or a descriptor field, because an author-settable barrier deadline is a way to configure
+ * a page-wide freeze.
+ */
+ @Test void a03_sourceShape_barrierDeadlineIsNotConfigurable() throws Exception {
+ var body = RegionsHarness.regionsJs();
+ assertTrue(body.contains("const BARRIER_DEADLINE_MS = 2000;"), body);
+ // Exactly one assignment in the whole file - the declaration - so there is no setter and no reassignment...
+ assertEquals(1, body.split("BARRIER_DEADLINE_MS = ", -1).length - 1,
+ "BARRIER_DEADLINE_MS must be assigned exactly once, at its declaration");
+ // ...and the deadline is never resolved from configuration: no attribute read, no global, no descriptor field.
+ for (var configPath : List.of(
+ "data-juneau-region-barrier", "data-juneau-barrier", "JUNEAU_BARRIER", "barrierMs", "barrierDeadline"))
+ assertFalse(body.contains(configPath), "the barrier deadline must not be readable from " + configPath);
+ }
+
+ /**
+ * The two caps are DECLARED SEPARATELY with different values and different units, and neither is defined in terms
+ * of the other. Conflating them was the defect this pair of constants exists to prevent: a shared ceiling
+ * reports a cycle for legitimate non-cyclic traffic.
+ */
+ @Test void a04_sourceShape_theTwoCapsAreIndependentConstants() throws Exception {
+ var body = RegionsHarness.regionsJs();
+ assertTrue(body.contains("const BUS_QUEUE_DEPTH_CAP = 64;"), body);
+ assertTrue(body.contains("const REPLAY_BUFFER_CAP = 256;"), body);
+ assertFalse(body.contains("REPLAY_BUFFER_CAP = BUS_QUEUE_DEPTH_CAP"), body);
+ assertFalse(body.contains("BUS_QUEUE_DEPTH_CAP * "), body);
+ }
+
+ // =================================================================================================================
+ // Behavior.
+ // =================================================================================================================
+
+ /** The container is EMPTY and IN-DOCUMENT at the first call, for each of the three region types. */
+ @Test void b01_containerIsEmptyAndInDocumentAtFirstCall() {
+ var r = report();
+ assertEquals(List.of("card-body", "row-detail", "tab-body"), r.get("t1_types"), r::toString);
+ assertAllTrue(r, "t1_allEmpty", "t1_allConnected", "t1_allSameNode", "t1_loadingViaAttrNotDom");
+ }
+
+ /**
+ * THE {@code ctx} GOLDEN. The full key set, the full {@code declared} key set and the full {@code ids} key set,
+ * asserted as literals - including SD-3's rule that a member absent for one region type is present and null
+ * rather than missing, so a populator can read {@code ctx.ids.sectionId} without a type check.
+ */
+ @Test void b02_ctxFieldSetIsAnExactGolden() {
+ var r = report();
+ assertEquals(List.of(
+ "contractVersion", "data", "declared", "defaultPopulate", "emit", "fetchDeclared", "generation",
+ "helpers", "host", "id", "ids", "key", "messageSignal", "on", "params", "reason", "refresh",
+ "selection", "signal", "type", "write"), r.get("t2_keys"), r::toString);
+ assertEquals(List.of("dataUrl", "fields", "lazy", "refreshMs", "renderer", "titleFields"),
+ r.get("t2_declaredKeys"), r::toString);
+ assertEquals(List.of("cardId", "gridId", "pageId", "rowId", "sectionId", "subtabId", "tabId", "viewId"),
+ r.get("t2_idsKeys"), r::toString);
+
+ var types = (Map<?,?>)r.get("t2_types");
+ for (var fn : List.of("defaultPopulate", "emit", "fetchDeclared", "on", "refresh", "write"))
+ assertEquals("function", types.get(fn), () -> fn + " must be a function on ctx");
+ assertEquals("string", types.get("contractVersion"));
+ assertEquals("number", types.get("generation"));
+
+ assertEquals("1", r.get("t2_contractVersion"));
+ assertEquals("row-detail", r.get("t2_type"));
+ assertEquals("diagnose", r.get("t2_id"));
+ assertEquals("gacks/row-42", r.get("t2_host"));
+ assertEquals("gacks/row-42/diagnose", r.get("t2_key"), "key is host + '/' + id");
+ assertEquals("initial", r.get("t2_reason"));
+ assertEquals(0, ((Number)r.get("t2_generation")).intValue());
+ assertAllTrue(r, "t2_messageSignalNullOnInitial", "t2_dataNull", "t2_selectionNull", "t2_paramsIsObject");
+ assertEquals("glance", r.get("t2_idsGrid"));
+ assertEquals("posture", r.get("t2_idsCard"));
+ assertAllTrue(r, "t2_idsSectionNullOnRowDetail", "t2_declaredFieldsNull", "t2_declaredTitleFieldsNull");
+ }
+
+ /**
+ * {@code ctx.signal.fork()}'s ATTACHMENT MECHANICS, in six parts. {@code fork} is a non-standard method on a
+ * standard type, so each of these is a distinct way to get it subtly wrong: a look-alike object the platform's own
+ * {@code fetch} would reject, an enumerable property that leaks into serialization, a prototype patch that
+ * mutates {@code AbortSignal} for every script on the page, a chainable fork that lets the tree grow past two
+ * levels, a supersede that leaves the previous child live, and a parent abort that misses its children.
+ */
+ @Test void b08a_forkAttachmentMechanics() {
+ var r = report();
+ assertAllTrue(r,
+ "t8a_isRealSignal", "t8a_signalMembers", "t8a_acceptedByRealFetch",
+ "t8a_own", "t8a_notInOwnKeys", "t8a_notOnPrototype",
+ "t8a_childIsSignal", "t8a_childHasNoFork",
+ "t8a_firstChildAborted", "t8a_secondChildLive", "t8a_generationIncremented",
+ "t8a_parentAbortKillsChild");
+ assertEquals("function", r.get("t8a_forkTypeof"));
+ assertEquals(false, r.get("t8a_enumerable"), "fork must be non-enumerable");
+ assertEquals(false, r.get("t8a_writable"), "fork must be non-writable");
+ }
+
+ /** A second enrolment walk over the same node re-populates NOTHING, and a re-populate leaves one copy, not two. */
+ @Test void b03_repopulateIsIdempotent() {
+ var r = report();
+ assertEquals(true, r.get("t3_sameHandle"), r::toString);
+ assertEquals(1, ((Number)r.get("t3_callsAfterSecondInit")).intValue(), "initRegion must be idempotent");
+ assertEquals(2, ((Number)r.get("t3_callsAfterRefresh")).intValue());
+ assertEquals(1, ((Number)r.get("t3_spanCount")).intValue(), "the container is cleared before every call");
+ }
+
+ /**
+ * A throwing populate is CONTAINED - the region errors, its siblings stay healthy - and an unknown populator name
+ * fails VISIBLY rather than blank. A region has no raw value to fall back to, unlike a renderer, so a blank
+ * container with no error would read to the user as "no data" rather than "broken page".
+ */
+ @Test void b04_throwingPopulateIsContainedAndUnknownNameFailsVisibly() {
+ var r = report();
+ assertEquals("error", r.get("t4_badState"));
+ assertEquals("ok", r.get("t4_goodState"));
+ assertAllTrue(r, "t4_goodHasContent", "t4_errorNamesRegion", "t4_errorVisibleInContainer");
+ assertEquals("error", r.get("t4_unknownState"));
+ assertAllTrue(r, "t4_unknownErrorNamesName", "t4_reservedRefused", "t4_reservedErrorLogged");
+ }
+
+ /** A Promise-returning populate goes loading -> ok only AFTER the settle. */
+ @Test void b05_promiseReturningPopulateStaysLoadingUntilSettle() {
+ var r = report();
+ assertEquals("loading", r.get("t5_stateWhileInFlight"));
+ assertEquals("ok", r.get("t5_stateAfterSettle"));
+ }
+
+ /**
+ * The four return-value shapes, and specifically {@code Promise<() => void>}: a cleanup RESOLVED FROM a promise is
+ * registered and invoked once, while a REJECTED populate registers none - a populate that failed before it
+ * finished allocating cannot be trusted to know what to release.
+ */
+ @Test void b06a_cleanupResolvedFromAPromiseIsInvokedExactlyOnce() {
+ var r = report();
+ assertEquals(0, ((Number)r.get("t6a_cleanupsBeforeTeardown")).intValue());
+ assertEquals(1, ((Number)r.get("t6a_cleanupsAfterTeardown")).intValue());
+ assertEquals(1, ((Number)r.get("t6a_cleanupsAfterSecondTeardown")).intValue(), "teardown is idempotent");
+ assertEquals("error", r.get("t6a_rejectedState"));
+ assertEquals(true, r.get("t6a_rejectedRegistersNoCleanup"), r::toString);
+ }
+
+ /**
+ * THE TEARDOWN ORDER, as an ORDER-RECORDING test rather than a set of independent "was it done" checks. The five
+ * steps are individually plausible in any order and each one is still observably "done" under a reordering, so
+ * only the recorded sequence catches a refactor that reorders them: the poll timer must be cleared BEFORE the
+ * abort or a timer can tick after teardown, the cleanup must run BEFORE the container is cleared or an author's
+ * cleanup reads an emptied container, and the unsubscribe must run BEFORE the queue is cleared or the
+ * subscription leaks.
+ */
+ @Test void b07_teardownStepsRunInTheirFixedOrder() {
+ var r = report();
+ assertEquals(List.of("abort", "cleanup"), r.get("t7_order"), r::toString);
+ assertAllTrue(r,
+ "t7_pollTimerClearedBeforeAbort", "t7_pollTimerCleared", "t7_pollNeverFired",
+ "t6_cleanupsExactlyOnce", "t6_containerNotYetClearedAtCleanup", "t7_stillSubscribedAtCleanup",
+ "t7_containerClearedAfter", "t7_markRemoved", "t7_unsubscribedAfterTeardown");
+ }
+
+ /** {@code ctx.signal} aborts on collapse, TRANSITIVELY killing every {@code fork()} child. */
+ @Test void b08_signalAbortsTransitivelyOnCollapse() {
+ var r = report();
+ assertAllTrue(r, "t8_liveBeforeTeardown", "t8_topLevelAborted", "t8_forkChildAborted",
+ "t8_abortReasonNamesTeardown");
+ }
+
+ /**
+ * {@code reason} tracks the CAUSAL EVENT, not the API that was invoked: an identical {@code ctx.refresh()} call is
+ * {@code "message"} from inside a bus handler and {@code "refresh"} from a timer, and {@code messageSignal} is
+ * non-null in exactly the first case. A populate that calls {@code refresh()} unconditionally spins exactly once
+ * - two invocations total - rather than forever.
+ */
+ @Test void b09a_reasonTracksTheCausalEventNotTheApi() {
+ var r = report();
+ assertEquals("initial", r.get("t9a_initial"));
+ assertEquals("message", r.get("t9a_fromHandler"));
+ assertEquals("refresh", r.get("t9a_fromTimer"));
+ assertAllTrue(r, "t9a_messageSignalNonNullOnMessage", "t9a_messageSignalNullOnRefresh",
+ "t9a_priorMessageSignalAborted");
+ assertEquals(2, ((Number)r.get("t9a_selfRefreshSpinsOnce")).intValue(),
+ "a populate that refreshes itself spins exactly once");
+ }
+
+ /** A region in flight is not RE-ENTERED: concurrent triggers coalesce into one follow-up run. */
+ @Test void b09b_concurrentTriggersCoalesceRatherThanReenter() {
+ var r = report();
+ assertEquals(1, ((Number)r.get("t_coalesce_callsWhileInFlight")).intValue());
+ assertEquals(2, ((Number)r.get("t_coalesce_callsAfterSettle")).intValue(), "three triggers, one follow-up run");
+ assertEquals(List.of("initial", "refresh"), r.get("t_coalesce_reasons"), r::toString);
+ assertEquals(true, r.get("t_coalesce_inFlightSignalAborted"), r::toString);
+ }
+
+ /**
+ * The enrolment walk: synchronous, idempotent, and orthogonal to barrier membership. A HIDDEN region is
+ * ENROLLED but not populated, and its first populate is {@code "activate"} and never {@code "initial"} - the
+ * distinction that keeps a tabbed page from waiting on cards the user cannot see.
+ */
+ @Test void b10_enrolmentAndActivationAreSeparateFromPopulation() {
+ var r = report();
+ assertEquals(2, ((Number)r.get("t_enrol_count")).intValue());
+ assertEquals(List.of("initial"), r.get("t_enrol_visiblePopulated"), r::toString);
+ assertAllTrue(r, "t_enrol_hiddenNotPopulated", "t_enrol_hiddenIsEnrolled");
+ assertEquals("idle", r.get("t_enrol_hiddenState"));
+ assertEquals(List.of("activate"), r.get("t_enrol_hiddenReasonOnActivate"), r::toString);
+ assertAllTrue(r, "t_enrol_activateOnce", "t_teardown_swept", "t_teardown_idempotent", "t_orphan_errorLogged");
+ }
+
+ /**
+ * {@code ctx.write} from a NON-TABLE region, which is the case that has no table to inherit a CSRF token from.
+ * The three refusals - safe method, cross-origin URL, traversal - each issue ZERO network traffic, and a missing
+ * token is a refusal rather than a silent unauthenticated send. None of the four touches the region's
+ * loading/ok/error state: a write is not a paint.
+ */
+ @Test void b50b_writeFromANonTableRegionStampsRefusesAndSupersedes() {
+ var r = report();
+ assertEquals("POST", r.get("t50b_method"));
+ assertAllTrue(r, "t50b_headerStamped", "t50b_contentType", "t50b_idempotencyMerged", "t50b_notBareCtxSignal",
+ "t50b_priorWriteAborted", "t50b_secondWriteLive", "t50b_nonTwoHundredResolvesNotRejects",
+ "t50b_refusalsIssuedNoTraffic", "t50b_refusalVisible", "t50b_stateUntouched");
+ assertEquals(Map.of("safeMethod", "safe-method", "crossOrigin", "unsafe-url", "dotDot", "unsafe-url"),
+ r.get("t50b_refusalReasons"), r::toString);
+ assertEquals("missing-token", r.get("t50b_missingTokenReason"));
+ }
+
+ /**
+ * THE {@code AbortController} BASELINE FAILS LOUD. A page of three regions in an environment without
+ * AbortController populates nothing, errors every region, logs EXACTLY ONE console error for the page rather than
+ * one per region, and hands out NO {@code ctx} at all - so there is no invocation whose {@code signal} is
+ * undefined or a polyfilled look-alike for a populator to trip over. The design's answer here is a refusal, not a
+ * degraded mode, because a degraded mode's failure is silent.
+ */
+ @Test void b08b_missingAbortControllerRefusesTheWholePageLoudlyAndExactlyOnce() {
+ var r = report();
+ assertEquals(0, ((Number)r.get("t8b_populates")).intValue(), "no populator may run without AbortController");
+ assertEquals(List.of("error", "error", "error"), r.get("t8b_allErrored"), r::toString);
+ assertEquals(1, ((Number)r.get("t8b_totalErrors")).intValue(), "one error for the page, not one per region");
+ assertAllTrue(r, "t8b_noHandles", "t8b_exactlyOneError", "t8b_noCtxHandedOut", "t8b_messageVisible");
+ }
+
+ /**
+ * The published surface, and the CONTRACT VERSION's independence. {@code RegionDef.CONTRACT_VERSION} is the
+ * region envelope's own and is not tied to the view or row-detail envelopes: the three evolve separately, and
+ * tying them would force a version bump on every consumer of either.
+ */
+ @Test void b11_publishedSurfaceAndIndependentContractVersion() {
+ var r = report();
+ assertEquals(List.of("function", "function", "function", "function"), r.get("tx_exports"), r::toString);
+ assertEquals(RegionDef.CONTRACT_VERSION, r.get("tx_contractVersion"),
+ "the JS contract version must match RegionDef.CONTRACT_VERSION");
+ assertEquals(List.of("row-detail", "card-body", "tab-body"), r.get("tx_regionTypes"), r::toString);
+ assertEquals(List.of("default"), r.get("tx_reserved"), r::toString);
+ assertEquals(true, r.get("tx_publishedOnSharedNamespace"), r::toString);
+ var caps = (Map<?,?>)r.get("tx_caps");
+ assertEquals(64, ((Number)caps.get("queue")).intValue());
+ assertEquals(256, ((Number)caps.get("replay")).intValue());
+ assertEquals(2000, ((Number)caps.get("deadline")).intValue());
+ }
+}
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsMixin_Serving_Test.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsMixin_Serving_Test.java
index 0a7d9568b7..cecdb47add 100644
--- a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsMixin_Serving_Test.java
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsMixin_Serving_Test.java
@@ -170,6 +170,7 @@ class ViewsMixin_Serving_Test extends TestBase {
cNoMixin.get(ViewsMixin.VIEWS_CSS_PATH).run().assertStatus(404);
cNoMixin.get(ViewsMixin.CONFIG_JS_PATH).run().assertStatus(404);
cNoMixin.get(ViewsMixin.CONFIG_CSS_PATH).run().assertStatus(404);
+ cNoMixin.get(ViewsMixin.REGIONS_JS_PATH).run().assertStatus(404);
}
@Test void h01_hostWithoutMixin_iconsJsRouteIs404() throws Exception {
@@ -217,6 +218,14 @@ class ViewsMixin_Serving_Test extends TestBase {
.assertContent().asString().isContains("juneau-renders.js");
}
+ @Test void b12_regionsJs_served() throws Exception {
+ cWithMixin.get(ViewsMixin.REGIONS_JS_PATH).run()
+ .assertStatus(200)
+ .assertHeader("Content-Type").isContains("text/javascript")
+ .assertHeader("Cache-Control").isContains("max-age")
+ .assertContent().asString().isContains("juneau-regions.js");
+ }
+
@Test void b04_viewsCss_served() throws Exception {
cWithMixin.get(ViewsMixin.VIEWS_CSS_PATH).run()
.assertStatus(200)
@@ -330,7 +339,7 @@ class ViewsMixin_Serving_Test extends TestBase {
}
@Test void c02_viewAssetUrl_worksForEveryAssetPath() {
- for (var path : new String[]{ViewsMixin.VIEWS_JS_PATH, ViewsMixin.RIBBON_JS_PATH, ViewsMixin.RENDERS_JS_PATH, ViewsMixin.VIEWS_CSS_PATH, ViewsMixin.ICONS_JS_PATH, ViewsMixin.SYMBOLS_SVG_PATH, ViewsMixin.CONFIG_JS_PATH, ViewsMixin.CONFIG_CSS_PATH})
+ for (var path : new String[]{ViewsMixin.VIEWS_JS_PATH, ViewsMixin.RIBBON_JS_PATH, ViewsMixin.RENDERS_JS_PATH, ViewsMixin.VIEWS_CSS_PATH, ViewsMixin.ICONS_JS_PATH, ViewsMixin.SYMBOLS_SVG_PATH, ViewsMixin.REGIONS_JS_PATH, ViewsMixin.CONFIG_JS_PATH, ViewsMixin.CONFIG_CSS_PATH})
assertTrue(ViewsMixin.viewAssetUrl(path).contains("?v="), path);
}
@@ -354,7 +363,7 @@ class ViewsMixin_Serving_Test extends TestBase {
for (var path : new String[]{
ViewsMixin.VIEWS_JS_PATH, ViewsMixin.RIBBON_JS_PATH, ViewsMixin.RENDERS_JS_PATH,
ViewsMixin.VIEWS_CSS_PATH, ViewsMixin.ICONS_JS_PATH, ViewsMixin.SYMBOLS_SVG_PATH, ViewsMixin.PAGES_JS_PATH,
- ViewsMixin.CONFIG_JS_PATH, ViewsMixin.CONFIG_CSS_PATH}) {
+ ViewsMixin.REGIONS_JS_PATH, ViewsMixin.CONFIG_JS_PATH, ViewsMixin.CONFIG_CSS_PATH}) {
var servedBytes = cWithMixin.get(path).run().assertStatus(200).getContent().asBytes();
var expectedHash = ChecksumUtils.hash8(servedBytes);
var url = ViewsMixin.viewAssetUrl(path);
diff --git a/juneau-rest/juneau-rest-server-views/src/test/js/regions-barrier.cjs b/juneau-rest/juneau-rest-server-views/src/test/js/regions-barrier.cjs
new file mode 100644
index 0000000000..1747577452
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-views/src/test/js/regions-barrier.cjs
@@ -0,0 +1,736 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * regions-barrier.cjs - always-on Node harness for the page-scoped initial-broadcast barrier and its bounded replay
+ * window: the arming protocol and its loud backstop, the three non-settlement modes, the 2000ms deadline, the
+ * released-region replay window with its membership-snapshot and last-window-close rules, the two INDEPENDENT caps,
+ * and enrolled-but-hidden not being a barrier member.
+ *
+ * The barrier's membership set is produced by the chrome enrolment walk, which is a later edit; everything here runs
+ * against a SYNTHETIC enrolment set built from initRegion/enrolIn plus registerRuntime/ready directly, which is the
+ * layer the barrier's own discipline lives at. The host-level arms - a real card grid, a real tabbed page shell -
+ * re-run against the three real hosts once those call sites exist.
+ *
+ * Several assertions are against the CLOCK rather than against delivery order, deliberately: for a hidden enrolled
+ * card, delivery is correct under both the right and the wrong reading and only the latency differs, so an
+ * order-based assertion passes either way.
+ *
+ * Usage: node regions-barrier.cjs <juneau-renders.js> <juneau-views.js> <juneau-regions.js>
+ */
+'use strict';
+
+const path = require('node:path');
+const H = require(path.join(__dirname, 'regions-harness.cjs'));
+
+const [, , rendersJsPath, viewsJsPath, regionsJsPath] = process.argv;
+if (!rendersJsPath || !viewsJsPath || !regionsJsPath) {
+ console.error('usage: node regions-barrier.cjs <juneau-renders.js> <juneau-views.js> <juneau-regions.js>');
+ process.exit(2);
+}
+
+const out = {};
+
+/** Loads a page and returns the harness plus a shared receive log stamped with the clock reading at delivery. */
+function scene() {
+ const h = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ h.log = {};
+ h.note = function (id, msg) {
+ (h.log[id] = h.log[id] || []).push({ kind: msg.kind, at: h.clock.now() });
+ };
+ h.kinds = function (id) { return (h.log[id] || []).map(function (r) { return r.kind; }); };
+ h.at = function (id) { return (h.log[id] || []).map(function (r) { return r.at; }); };
+ return h;
+}
+
+/** A subscriber region: subscribes synchronously during its own initial populate, then settles as told. */
+function subscriber(h, id, opts) {
+ opts = opts || {};
+ h.R.register('sub-' + id, function (ctx, container) {
+ if (opts.captureCtx) opts.captureCtx(ctx, container);
+ if (opts.subscribeNow !== false) ctx.on(function (msg) { h.note(id, msg); });
+ if (opts.returns) return opts.returns(ctx, container);
+ return undefined;
+ });
+ const el = H.mkRegion(h.env, {
+ id: id,
+ type: opts.type || 'card-body',
+ host: opts.host,
+ populate: 'sub-' + id,
+ parent: opts.parent,
+ hiddenPanel: opts.hiddenPanel
+ });
+ if (opts.enrol !== false) h.R.initRegion(el);
+ return el;
+}
+
+/** A control region that emits during its own initial populate - the case the whole barrier exists for. */
+function control(h, id, msg, opts) {
+ opts = opts || {};
+ h.R.register('ctl-' + id, function (ctx) {
+ ctx.emit(msg);
+ if (opts.returns) return opts.returns(ctx);
+ return undefined;
+ });
+ const el = H.mkRegion(h.env, { id: id, type: 'card-body', host: opts.host, populate: 'ctl-' + id, parent: opts.parent });
+ h.R.initRegion(el);
+ return el;
+}
+
+/** An emitter region enrolled AFTER the barrier lifted, so its emit is an ordinary top-of-turn broadcast. */
+function emitOnce(h, id, msg) {
+ h.R.register('em-' + id, function (ctx) { ctx.emit(msg); });
+ const el = H.mkRegion(h.env, { id: id, type: 'card-body', populate: 'em-' + id });
+ h.R.initRegion(el);
+ return el;
+}
+
+(async function () {
+
+ // =================================================================================================================
+ // Test 49(a) - a control region that emits during its own reason:"initial" populate and initializes FIRST still
+ // reaches three chart regions that subscribe during their own LATER initial populates - including one whose
+ // populate returns a Promise that settles after the control's, so the barrier is proven to wait on async
+ // populates and not merely on synchronous return.
+ // =================================================================================================================
+ {
+ const h = scene();
+ h.R.registerRuntime('test');
+ const held = H.deferred();
+ control(h, 'ctl', { kind: 'M1' });
+ subscriber(h, 'chart1');
+ subscriber(h, 'chart2');
+ subscriber(h, 'chart3', { returns: function () { return held.promise; } });
+ h.R.ready('test');
+ out.t49a_beforeAsyncSettle = [h.kinds('chart1'), h.kinds('chart2'), h.kinds('chart3')];
+ held.resolve();
+ await H.flush();
+ out.t49a_afterAsyncSettle = [h.kinds('chart1'), h.kinds('chart2'), h.kinds('chart3')];
+ out.t49a_drainedAtZeroNotDeadline = h.at('chart1').concat(h.at('chart3'));
+ out.t49a_noDeadlineWarning = h.rec.warnsMatching('deadline').length === 0;
+ out.t49a_controlDidNotReceiveItsOwn = h.kinds('ctl').length === 0;
+
+ // Test 49(b) - a region activated LATER receives NOTHING retroactively: the design declines replay-on-subscribe
+ // for a region that was never a barrier member.
+ const late = subscriber(h, 'late', { hiddenPanel: true });
+ H.activatePanel(late);
+ h.R.activateRegion(late._juneauRegion);
+ out.t49b_lateActivatedGetsNothing = h.kinds('late');
+ }
+
+ // =================================================================================================================
+ // Test 49a - a SYNCHRONOUSLY-THROWING initial populate releases the barrier (non-settlement mode 1).
+ // =================================================================================================================
+ {
+ const h = scene();
+ h.R.registerRuntime('test');
+ const held = H.deferred();
+ control(h, 'ctl', { kind: 'M1' });
+ subscriber(h, 'r1');
+ h.R.register('thrower', function () { throw new Error('sync boom'); });
+ const bad = H.mkRegion(h.env, { id: 'bad', type: 'card-body', populate: 'thrower' });
+ h.R.initRegion(bad);
+ subscriber(h, 'r3', { returns: function () { return held.promise; } });
+ h.R.ready('test');
+ out.t49a1_heldUntilThirdSettles = [h.kinds('r1'), h.kinds('r3')];
+ held.resolve();
+ await H.flush();
+ out.t49a1_drainedOnThirdSettle = [h.kinds('r1'), h.kinds('r3')];
+ out.t49a1_drainedAtZero = h.at('r1');
+ out.t49a1_throwerErrored = bad.getAttribute('data-juneau-region-state');
+ out.t49a1_noDeadlineNeeded = h.rec.warnsMatching('deadline').length === 0;
+ }
+
+ // =================================================================================================================
+ // Test 49b - a REJECTED initial populate releases the barrier (mode 2). Worth separating from 49a because the
+ // natural implementation - a try around a synchronous call - catches the throw and NOT the rejection, and the
+ // symptom of missing the second is a page-wide freeze with a green-looking console.
+ // =================================================================================================================
+ {
+ const h = scene();
+ h.R.registerRuntime('test');
+ const rejected = H.deferred();
+ control(h, 'ctl', { kind: 'M1' });
+ subscriber(h, 'r1');
+ h.R.register('rejector', function () { return rejected.promise; });
+ const bad = H.mkRegion(h.env, { id: 'bad', type: 'card-body', populate: 'rejector' });
+ h.R.initRegion(bad);
+ h.R.ready('test');
+ out.t49b_heldWhileRejectionPending = h.kinds('r1');
+ rejected.reject(new Error('async boom'));
+ await H.flush();
+ out.t49b_drainedOnRejection = h.kinds('r1');
+ out.t49b_drainedAtZero = h.at('r1');
+ out.t49b_rejectorErrored = bad.getAttribute('data-juneau-region-state');
+ out.t49b_noDeadlineNeeded = h.rec.warnsMatching('deadline').length === 0;
+ }
+
+ // =================================================================================================================
+ // Test 49c - a NEVER-SETTLING initial populate releases the barrier AT THE DEADLINE (mode 3). This is the test
+ // for the failure rated worse than the bug it replaced: a barrier that buffers every emit while any region is in
+ // its initial populate freezes all subsequent user-driven traffic for the life of the page, not just the initial
+ // broadcast.
+ // =================================================================================================================
+ {
+ const h = scene();
+ h.R.registerRuntime('test');
+ control(h, 'ctl', { kind: 'M1' });
+ subscriber(h, 'healthy');
+ let hungContainer = null;
+ let hungSignal = null;
+ const hung = subscriber(h, 'hung', {
+ subscribeNow: false,
+ captureCtx: function (ctx, container) {
+ hungSignal = ctx.signal;
+ container.appendChild(h.env.el('i'));
+ hungContainer = container;
+ },
+ returns: function () { return new Promise(function () { /* never settles */ }); }
+ });
+ h.R.ready('test');
+ out.t49c_heldBeforeDeadline = h.kinds('healthy');
+ h.clock.advance(1999);
+ out.t49c_stillHeldAt1999 = h.kinds('healthy');
+ h.clock.advance(1);
+ out.t49c_drainedAtDeadline = h.kinds('healthy');
+ out.t49c_deliveredAt = h.at('healthy');
+ out.t49c_warningNamesRegion = h.rec.warnsMatching('hung').length === 1;
+ out.t49c_oneWarningForThePage = h.rec.warnsMatching('initial-broadcast barrier reached').length === 1;
+ // The hung populate is NOT aborted and its container is untouched: the barrier is an ordering device, not a
+ // lifecycle one.
+ out.t49c_hungNotAborted = hungSignal.aborted === false;
+ out.t49c_hungContainerUntouched = hungContainer.querySelectorAll('i').length === 1;
+ out.t49c_hungStillLoading = hung.getAttribute('data-juneau-region-state');
+ // ...and a later user-driven emit is delivered normally rather than frozen for the life of the page.
+ h.clock.advance(5000);
+ emitOnce(h, 'later', { kind: 'M2' });
+ out.t49c_laterTrafficFlows = h.kinds('healthy');
+ }
+
+ // =================================================================================================================
+ // Test 49d - the deadline is a CLAMPED CONSTANT, not settable from a descriptor, a bean or a global. An
+ // author-settable barrier deadline is a way to configure a page-wide freeze.
+ // =================================================================================================================
+ {
+ const h = scene();
+ out.t49d_value = h.R.BARRIER_DEADLINE_MS;
+ // Not settable through the published namespace...
+ try { h.R.BARRIER_DEADLINE_MS = 5; } catch (e) { /* strict-mode refusal is also an acceptable answer */ }
+ // ...nor through a global the runtime might have consulted...
+ h.env.window.JUNEAU_BARRIER_DEADLINE_MS = 5;
+ h.env.window.juneauBarrierDeadlineMs = 5;
+ // ...nor through an attribute on the region or on its host.
+ h.R.registerRuntime('test');
+ const el = subscriber(h, 'slow', { enrol: false, returns: function () { return new Promise(function () {}); } });
+ el.setAttribute('data-juneau-region-barrier-ms', '5');
+ el.setAttribute('data-juneau-barrier-deadline', '5');
+ h.env.body.setAttribute('data-juneau-barrier-deadline', '5');
+ h.R.initRegion(el);
+ control(h, 'ctl', { kind: 'M1' });
+ subscriber(h, 'healthy');
+ h.R.ready('test');
+ h.clock.advance(5);
+ out.t49d_notReleasedAtFive = h.kinds('healthy');
+ h.clock.advance(1995);
+ out.t49d_releasedAtTwoThousand = h.kinds('healthy');
+ out.t49d_deliveredAt = h.at('healthy');
+ }
+
+ // =================================================================================================================
+ // Test 49e - THE TWO-HOST CASE. A page with both a card grid and a tabbed page shell, and the barrier must hold
+ // across two independently-booting runtimes that have no reference to each other. Under a host-scoped barrier
+ // sub-case (i) fails, and it fails intermittently, which is the worst way for it to fail.
+ // =================================================================================================================
+ {
+ const h = scene();
+ h.R.registerRuntime('cards');
+ h.R.registerRuntime('pages');
+
+ // The grid boots first and its control card emits during its initial populate.
+ const grid = h.env.el('div');
+ grid.setAttribute('data-juneau-card-grid', 'glance');
+ h.env.body.appendChild(grid);
+ control(h, 'ctl', { kind: 'M1' }, { parent: grid, host: 'glance' });
+ subscriber(h, 'gridChart', { parent: grid, host: 'glance' });
+ h.R.ready('cards');
+ out.t49e_notDrainedAfterFirstRuntime = h.kinds('gridChart');
+
+ // The page shell boots second. Its INITIAL panel's regions enrol before the barrier lifts...
+ const initialPanel = h.env.el('div');
+ initialPanel.className = 'jc-panel jc-active';
+ h.env.body.appendChild(initialPanel);
+ subscriber(h, 'panelChart', { parent: initialPanel, type: 'tab-body', host: 'repo-a' });
+ // ...and a NON-INITIAL panel carries both a naked tab-body region and a CARD, both enrolled while hidden.
+ const lazyTab = subscriber(h, 'lazyTab', { hiddenPanel: true, type: 'tab-body', host: 'repo-b' });
+ const lazyCard = subscriber(h, 'lazyCard', { hiddenPanel: true, type: 'card-body', host: 'repo-b' });
+ h.R.ready('pages');
+
+ // (i) The barrier held across both runtimes, and the initial panel's regions received the broadcast.
+ out.t49e_i_gridChart = h.kinds('gridChart');
+ out.t49e_i_panelChart = h.kinds('panelChart');
+ out.t49e_i_drainedAtZero = h.at('gridChart').concat(h.at('panelChart'));
+
+ // (ii)/(iii) The lazily activated tab's chart gets NOTHING, and so does the CARD in a non-initial panel -
+ // so this is a property of LATE ACTIVATION rather than of which runtime enrolled the region.
+ h.clock.advance(10000);
+ for (const el of [lazyTab, lazyCard]) {
+ H.activatePanel(el);
+ h.R.activateRegion(el._juneauRegion);
+ }
+ out.t49e_ii_lazyTab = h.kinds('lazyTab');
+ out.t49e_iii_lazyCard = h.kinds('lazyCard');
+ }
+
+ // =================================================================================================================
+ // Test 49f - THE ARMING PROTOCOL. (i) an empty outstanding set is held until every registered runtime has
+ // reported; (ii) a page with NO regions lifts immediately and adds no listener; (iii) a registered runtime that
+ // never reports has the barrier lift at DOMContentLoaded plus one macrotask, with ONE error naming it.
+ // =================================================================================================================
+ {
+ // (i) Two registered runtimes, the second reporting late. Every region has already settled, so the
+ // outstanding set is empty the whole time - only the ready protocol is holding the barrier.
+ const h = scene();
+ h.R.registerRuntime('cards');
+ h.R.registerRuntime('pages');
+ control(h, 'ctl', { kind: 'M1' });
+ subscriber(h, 'chart');
+ h.R.ready('cards');
+ out.t49f_i_heldOnEmptyOutstanding = h.kinds('chart');
+ h.R.ready('pages');
+ out.t49f_i_liftedOnSecondReport = h.kinds('chart');
+ out.t49f_i_liftedAtZero = h.at('chart');
+ }
+ {
+ // (ii) A page with NO regions: nothing arms, no timer is scheduled, no document listener is added, and an
+ // emit is delivered immediately rather than buffered. "A page with no regions pays nothing."
+ const h = scene();
+ out.t49f_ii_noTimers = h.clock.pending() === 0;
+ out.t49f_ii_noDocumentListeners = Object.keys(h.env.document).indexOf('DOMContentLoaded') < 0;
+ // Enrolling a region is the ONLY thing that arms it - loading the asset does not.
+ out.t49f_ii_assetLoadedButUnarmed = typeof h.R.initRegion === 'function' && h.clock.pending() === 0;
+ }
+ {
+ // (iii) The rule-4a backstop: a registered runtime that NEVER reports. The barrier lifts anyway, one
+ // macrotask after DOMContentLoaded, with exactly one error naming the forgetful runtime - so it degrades
+ // LOUDLY rather than silently, which is the inverse of the failure mode the clock-only design was rejected
+ // for.
+ const h = scene();
+ h.R.registerRuntime('forgetful');
+ control(h, 'ctl', { kind: 'M1' });
+ subscriber(h, 'chart');
+ out.t49f_iii_heldBeforeMacrotask = h.kinds('chart');
+ h.clock.advance(0);
+ out.t49f_iii_liftedAtMacrotask = h.kinds('chart');
+ out.t49f_iii_liftedAtZeroNotDeadline = h.at('chart');
+ out.t49f_iii_oneErrorNamingRuntime = h.rec.errorsMatching('forgetful').length === 1;
+ out.t49f_iii_errorMentionsReady = h.rec.errorsMatching('JuneauViews.regions.ready').length === 1;
+ }
+
+ // =================================================================================================================
+ // Test 49g - THE RELEASED-REGION REPLAY WINDOW DELIVERS. The healthy regions receive the broadcast at the
+ // deadline (no page-wide wait); the slow region's LATER ctx.on receives the same messages, in the same order,
+ // exactly once; and - the assertion that distinguishes a replay window from a retained cache - a subsequent live
+ // emit is delivered AFTER the replayed ones and is not duplicated.
+ // =================================================================================================================
+ {
+ const h = scene();
+ h.R.registerRuntime('test');
+ const slowFetch = H.deferred();
+ control(h, 'ctl', { kind: 'M1' });
+ subscriber(h, 'healthy');
+ // The §8.5 row-1 mix-wrapper shape, with the declarative delegation stubbed out (that half is a later
+ // child): await the declared fetch, THEN subscribe.
+ subscriber(h, 'slow', {
+ subscribeNow: false,
+ returns: function (ctx) {
+ return slowFetch.promise.then(function () { ctx.on(function (msg) { h.note('slow', msg); }); });
+ }
+ });
+ h.R.ready('test');
+ out.t49g_heldBeforeDeadline = h.kinds('healthy');
+ h.clock.advance(2000);
+ out.t49g_healthyGotItAtDeadline = h.kinds('healthy');
+ out.t49g_healthyAt = h.at('healthy');
+ out.t49g_slowNothingYet = h.kinds('slow');
+ // The slow region settles late, subscribes, and the replay hands it what it missed.
+ slowFetch.resolve();
+ await H.flush();
+ out.t49g_slowReplayed = h.kinds('slow');
+ out.t49g_slowReplayedOnce = h.kinds('slow').filter(function (k) { return k === 'M1'; }).length === 1;
+ // A subsequent LIVE emit lands after the replayed ones and is not duplicated.
+ emitOnce(h, 'later', { kind: 'M2' });
+ out.t49g_slowAfterLiveEmit = h.kinds('slow');
+ out.t49g_healthyAfterLiveEmit = h.kinds('healthy');
+ }
+
+ // =================================================================================================================
+ // Test 49h - THE REPLAY WINDOW CLOSES, AND IS NOT A RETAINED CACHE. Four negatives, because every one of them is
+ // a way the replay could grow into the retained-cache option that was rejected.
+ // =================================================================================================================
+ {
+ const h = scene();
+ h.R.registerRuntime('test');
+ const slowFetch = H.deferred();
+ const reads = [];
+ const trapped = new Proxy({ kind: 'M1' }, {
+ get: function (t, k) { reads.push(String(k)); return t[k]; },
+ has: function (t, k) { reads.push('has:' + String(k)); return k in t; },
+ ownKeys: function (t) { reads.push('ownKeys'); return Reflect.ownKeys(t); }
+ });
+ h.R.register('ctl', function (ctx) { ctx.emit(trapped); });
+ h.R.initRegion(H.mkRegion(h.env, { id: 'ctl', type: 'card-body', populate: 'ctl' }));
+ let slowCtx = null;
+ subscriber(h, 'slow', {
+ subscribeNow: false,
+ captureCtx: function (ctx) { slowCtx = ctx; },
+ returns: function () { return slowFetch.promise; }
+ });
+ h.R.ready('test');
+ h.clock.advance(2000);
+
+ // (i) A region that ENROLLED after the barrier lifted receives NOTHING, even while a released region's
+ // window is still open.
+ subscriber(h, 'lateEnrolled');
+ out.t49h_i_lateEnrolledGetsNothing = h.kinds('lateEnrolled');
+
+ // (iv) The replay reads NO payload property - what keeps payload opacity true on the replay path, and
+ // precisely the objection the retained-cache option failed. The handler captures the REFERENCE and reads
+ // nothing, so the only reads that could be recorded are the framework's own.
+ const got = [];
+ reads.length = 0;
+ slowCtx.on(function (msg) { got.push(msg); });
+ out.t49h_iv_replayReadsNothing = reads.length === 0;
+ out.t49h_iv_replayedIdentityPreserved = got.length === 1 && got[0] === trapped;
+ for (const m of got) h.note('slow', m);
+ out.t49h_iv_replayed = h.kinds('slow');
+
+ // (ii) A released region that subscribes AFTER ITS OWN POPULATE SETTLES receives nothing: the window closed
+ // with the settle.
+ slowFetch.resolve();
+ await H.flush();
+ slowCtx.on(function (msg) { h.note('slowAfterSettle', msg); });
+ out.t49h_ii_afterOwnSettleGetsNothing = h.kinds('slowAfterSettle');
+
+ // (iii) The buffer is DISCARDED once the last released window closes, asserted by a ctx.on from a fresh
+ // region afterwards receiving nothing.
+ subscriber(h, 'fresh');
+ out.t49h_iii_freshRegionGetsNothing = h.kinds('fresh');
+ }
+
+ // =================================================================================================================
+ // Test 49i - REPLAY IS A FAN-OUT, AND A STILL-OPEN SECOND WINDOW RECEIVES WHAT THE REPLAY EMITS. Run as two
+ // arms of the SAME page so the expected order is DERIVED from the ordinary delivery rules rather than asserted as
+ // a literal - which is what keeps the second delivery path from drifting out of agreement with the first.
+ // =================================================================================================================
+ {
+ const arms = {};
+ for (const arm of ['released', 'control']) {
+ const h = scene();
+ h.R.registerRuntime('test');
+ const bFetch = H.deferred();
+ const cFetch = H.deferred();
+ const trace = [];
+ control(h, 'ctl', { kind: 'M1' });
+ // A is healthy and subscribed throughout.
+ subscriber(h, 'A');
+ // B and C are mix wrappers: await the declared fetch, THEN subscribe. B re-emits on M1.
+ subscriber(h, 'B', {
+ subscribeNow: false,
+ returns: function (ctx) {
+ return bFetch.promise.then(function () {
+ ctx.on(function (msg) {
+ h.note('B', msg);
+ if (msg.kind === 'M1') {
+ ctx.emit({ kind: 'M2' });
+ // HALF ONE, asserted OBSERVABLY: at the instant this handler returns from emit, A has
+ // not yet received M2. It receives it only after this drain completes.
+ trace.push('A-had-M2-at-emit-return:' + (h.kinds('A').indexOf('M2') >= 0));
+ }
+ });
+ });
+ }
+ });
+ subscriber(h, 'C', {
+ subscribeNow: false,
+ returns: function (ctx) {
+ return cFetch.promise.then(function () {
+ ctx.on(function (msg) { h.note('C', msg); });
+ });
+ }
+ });
+ h.R.ready('test');
+
+ if (arm === 'control') {
+ // The CONTROL arm: both settle BEFORE the deadline, so no window ever opens and the ordinary
+ // delivery rules govern unmodified.
+ bFetch.resolve();
+ await H.flush();
+ cFetch.resolve();
+ await H.flush();
+ h.clock.advance(3000);
+ } else {
+ // The RELEASED arm: both are held past the deadline, B is released and replayed first, and C's fetch
+ // is still in flight - its window still open - when B's replayed handler emits M2.
+ h.clock.advance(2000);
+ bFetch.resolve();
+ await H.flush();
+ cFetch.resolve();
+ await H.flush();
+ }
+ arms[arm] = {
+ A: h.kinds('A'),
+ B: h.kinds('B'),
+ C: h.kinds('C'),
+ trace: trace,
+ cycleErrors: h.rec.errorsMatching('emit cycle detected').length
+ };
+
+ if (arm === 'released') {
+ // (iv) The bounds hold under appending: a late-ENROLLED region and a region enrolled at boot while
+ // HIDDEN both still receive nothing, and the buffer is discarded once the last window closes.
+ const lateEnrolled = subscriber(h, 'lateEnrolled');
+ const hiddenAtBoot = subscriber(h, 'hiddenAtBoot', { hiddenPanel: true });
+ H.activatePanel(hiddenAtBoot);
+ h.R.activateRegion(hiddenAtBoot._juneauRegion);
+ arms[arm].lateEnrolled = h.kinds('lateEnrolled');
+ arms[arm].hiddenAtBoot = h.kinds('hiddenAtBoot');
+ }
+ }
+ out.t49i_i_emitWasQueuedNotLive = arms.released.trace;
+ out.t49i_ii_cReceivedBoth = arms.released.C;
+ out.t49i_iii_identicalSequences =
+ JSON.stringify([arms.released.A, arms.released.B, arms.released.C])
+ === JSON.stringify([arms.control.A, arms.control.B, arms.control.C]);
+ out.t49i_iii_released = { A: arms.released.A, B: arms.released.B, C: arms.released.C };
+ out.t49i_iii_control = { A: arms.control.A, B: arms.control.B, C: arms.control.C };
+ out.t49i_iv_lateEnrolled = arms.released.lateEnrolled;
+ out.t49i_iv_hiddenAtBoot = arms.released.hiddenAtBoot;
+ out.t49i_noCycleError = arms.released.cycleErrors === 0 && arms.control.cycleErrors === 0;
+ }
+
+ // =================================================================================================================
+ // Test 49j - ENROLLED-BUT-HIDDEN IS NOT A BARRIER MEMBER. Enrolment and barrier membership are different axes,
+ // and the first assertion is the one with teeth: the drain must NOT be delayed by the hidden card, asserted
+ // against the CLOCK, because delivery is correct under both readings and only the latency differs.
+ // =================================================================================================================
+ {
+ const h = scene();
+ h.R.registerRuntime('cards');
+ const grid = h.env.el('div');
+ grid.setAttribute('data-juneau-card-grid', 'glance');
+ h.env.body.appendChild(grid);
+ control(h, 'ctl', { kind: 'M1' }, { parent: grid });
+ subscriber(h, 'visible', { parent: grid });
+ const hidden = subscriber(h, 'hiddenCard', { hiddenPanel: true, parent: grid });
+ h.R.ready('cards');
+
+ // (i) THE DRAIN IS NOT DELAYED BY THE HIDDEN CARD: the clock has advanced 0ms, not 2000.
+ out.t49j_i_deliveredAt = h.at('visible');
+ out.t49j_i_visibleGotIt = h.kinds('visible');
+ out.t49j_i_clockAtDelivery = h.clock.now();
+ out.t49j_i_noDeadlineWarning = h.rec.warnsMatching('deadline').length === 0;
+
+ // (ii) The hidden card's region IS enrolled: initRegion was called for it, it has a handle, and its teardown
+ // is reachable.
+ out.t49j_ii_enrolled = hidden._juneauRegion !== undefined && hidden._juneauRegion !== null;
+ out.t49j_ii_hasKey = hidden._juneauRegion.key;
+ out.t49j_ii_teardownReachable = typeof h.R.teardownRegion === 'function';
+
+ // (iii) It has had NO populate at all while hidden - which is WHY it is not in the outstanding set, rather
+ // than by a special case.
+ out.t49j_iii_state = hidden.getAttribute('data-juneau-region-state');
+ out.t49j_iii_noContent = hidden.childNodes.length === 0;
+
+ // (iv) Activating its panel later produces exactly one populate, reason:"activate", and it receives NOTHING
+ // retroactively - not M1, and no replay even if a released window is open at that moment.
+ const reasons = [];
+ h.R.register('sub-hiddenCard', function (ctx) {
+ reasons.push(ctx.reason);
+ ctx.on(function (msg) { h.note('hiddenCard', msg); });
+ });
+ H.activatePanel(hidden);
+ h.R.activateRegion(hidden._juneauRegion);
+ out.t49j_iv_reasons = reasons.slice();
+ out.t49j_iv_receivedNothing = h.kinds('hiddenCard');
+ h.R.activateRegion(hidden._juneauRegion);
+ out.t49j_iv_activateOnce = reasons.length === 1;
+ }
+
+ // =================================================================================================================
+ // Test 49k - THE REPLAY BUFFER'S OVERFLOW IS NOT A CYCLE ERROR. 257 legitimate, non-cyclic, user-driven
+ // broadcasts, each from a DIFFERENT emitter and each at the top of its own turn, so no fan-out is ever nested.
+ // =================================================================================================================
+ {
+ const h = scene();
+ h.R.registerRuntime('test');
+ const cFetch = H.deferred();
+ const dFetch = H.deferred();
+ let cCtx = null;
+ subscriber(h, 'C', {
+ subscribeNow: false,
+ captureCtx: function (ctx) { cCtx = ctx; },
+ returns: function (ctx) {
+ return cFetch.promise.then(function () { ctx.on(function (m) { h.note('C', m); }); });
+ }
+ });
+ subscriber(h, 'D', {
+ subscribeNow: false,
+ returns: function (ctx) {
+ return dFetch.promise.then(function () { ctx.on(function (m) { h.note('D', m); }); });
+ }
+ });
+ h.R.ready('test');
+ h.clock.advance(2000);
+ out.t49k_bothReleased = h.rec.warnsMatching('initial-broadcast barrier reached').length === 1;
+
+ const cap = h.R.REPLAY_BUFFER_CAP;
+ for (let i = 1; i <= cap + 1; i++) {
+ emitOnce(h, 'e' + i, { kind: 'm' + i });
+ if (i === 200) {
+ // (ii) D settles and subscribes at append 200: it replays exactly the first 200, in order, exactly
+ // once, and its window then closes - so D never reaches append 257 while C stays in flight through
+ // it. Two regions, two outcomes, both reachable.
+ dFetch.resolve();
+ await H.flush();
+ out.t49k_ii_dReplayCount = h.kinds('D').length;
+ out.t49k_ii_dReplayFirst = h.kinds('D')[0];
+ out.t49k_ii_dReplayLast = h.kinds('D')[h.kinds('D').length - 1];
+ out.t49k_ii_dInOrder = h.kinds('D').every(function (k, idx) { return k === 'm' + (idx + 1); });
+ }
+ }
+
+ // (i) NO CYCLE ERROR IS LOGGED, at any point. Under a single shared cap, append 65 raises one - a false
+ // diagnostic for traffic that contains no cycle, and worse than silence because it names a defect that does
+ // not exist.
+ out.t49k_i_noCycleError = h.rec.errorsMatching('emit cycle detected').length === 0;
+
+ // (iii) AT APPEND 257 THE BUFFER IS ABANDONED, LOUDLY. C receives NO replay at all rather than a truncated
+ // one - a run of messages with a hole is the outcome rated worse than none, because the region cannot detect
+ // it - and exactly one error names C's key, the cap, and the exactly-once failure.
+ const overflow = h.rec.errorsMatching('replay buffer exceeded its cap');
+ out.t49k_iii_exactlyOneOverflowError = overflow.length === 1;
+ out.t49k_iii_namesKey = overflow.length === 1 && overflow[0].indexOf(cCtx.key) >= 0;
+ out.t49k_iii_namesCap = overflow.length === 1 && overflow[0].indexOf(String(cap)) >= 0;
+ out.t49k_iii_namesExactlyOnce = overflow.length === 1 && overflow[0].indexOf('exactly-once') >= 0;
+ out.t49k_iii_saysNotACycle = overflow.length === 1 && overflow[0].indexOf('not an emit cycle') >= 0;
+ out.t49k_iii_namesOnlyTheOpenWindow = overflow.length === 1
+ && overflow[0].indexOf('NOT met for: ' + cCtx.key + '.') >= 0;
+ cFetch.resolve();
+ await H.flush();
+ out.t49k_iii_cReceivedNoReplay = h.kinds('C');
+ out.t49k_iii_noPartialReplay = h.kinds('C').length === 0;
+
+ // (iv) The two caps are INDEPENDENT, asserted in both directions. This 257-message page never raised the
+ // cycle error even though its buffer overflowed...
+ out.t49k_iv_bufferOverflowedWithoutCycleError = out.t49k_i_noCycleError === true
+ && out.t49k_iii_exactlyOneOverflowError === true;
+ out.t49k_iv_capsDiffer = h.R.BUS_QUEUE_DEPTH_CAP !== h.R.REPLAY_BUFFER_CAP;
+ }
+ {
+ // ...and a page that reaches the QUEUE depth cap by genuine nesting still raises the cycle error even though
+ // its buffer is short. Under one shared constant one of these two must fail.
+ const h = scene();
+ h.R.register('pong', function (ctx) {
+ ctx.on(function () { ctx.emit({ kind: 'pong' }, { to: ctx.id === 'p1' ? 'p2' : 'p1' }); });
+ });
+ let starter = null;
+ h.R.register('starter', function (ctx) { starter = ctx; });
+ for (const id of ['p1', 'p2']) h.R.initRegion(H.mkRegion(h.env, { id: id, type: 'card-body', populate: 'pong' }));
+ h.R.initRegion(H.mkRegion(h.env, { id: 's', type: 'card-body', populate: 'starter' }));
+ starter.emit({ kind: 'go' }, { to: 'p1' });
+ out.t49k_iv_nestingRaisesCycleError = h.rec.errorsMatching('emit cycle detected').length === 1;
+ out.t49k_iv_nestingRaisedNoOverflowError = h.rec.errorsMatching('replay buffer exceeded').length === 0;
+ }
+
+ // =================================================================================================================
+ // Test 49l - THE LAST-WINDOW CLOSE, WITH A GENUINELY SYNCHRONOUS SETTLE. A thenable with a synchronous `then`,
+ // so C's close condition becomes true PARTWAY THROUGH a live fan-out. Resolving an ordinary promise from inside a
+ // subscriber cannot reach this: resolving queues a microtask while the fan-out is synchronous, so the continuation
+ // runs after the drain and the assertion passes for the wrong reason.
+ // =================================================================================================================
+ {
+ const arms = {};
+ for (const bFirst of [true, false]) {
+ const h = scene();
+ h.R.registerRuntime('test');
+ const bFetch = H.deferred();
+ const cSync = H.syncThenable();
+ let cCtx = null;
+
+ // B subscribes synchronously during its own populate and settles BEFORE the live emit, so B is a live
+ // subscriber whose OWN window has already closed - leaving C as the LAST open window. Its handler is
+ // what makes C's close condition become true partway through the fan-out.
+ const mkB = function () {
+ subscriber(h, 'B', {
+ subscribeNow: false,
+ captureCtx: function (ctx) {
+ ctx.on(function (msg) {
+ h.note('B', msg);
+ if (msg.kind !== 'M3') return;
+ // C's settle path, collapsed onto a synchronous settle: subscribe, then resolve, both
+ // inside this handler's own turn.
+ cCtx.on(function (m) { h.note('C', m); });
+ cSync.settle();
+ });
+ },
+ returns: function () { return bFetch.promise; }
+ });
+ };
+ // D exists only to make subscription order controllable: the arms differ in whether B subscribes before
+ // or after it, and C's replayed sequence must be identical either way.
+ if (bFirst) { mkB(); subscriber(h, 'D'); } else { subscriber(h, 'D'); mkB(); }
+ subscriber(h, 'C', {
+ subscribeNow: false,
+ captureCtx: function (ctx) { cCtx = ctx; },
+ returns: function () { return cSync.thenable; }
+ });
+ h.R.ready('test');
+ h.clock.advance(2000);
+ bFetch.resolve();
+ await H.flush();
+
+ emitOnce(h, 'em3', { kind: 'M3' });
+ const armed = { C: h.kinds('C'), B: h.kinds('B'), D: h.kinds('D') };
+
+ // (iii) A second live emit AFTER the drain is not appended for C, and (iv) the buffer was discarded at
+ // that same boundary - both observed through a fresh region's subscribe receiving nothing.
+ emitOnce(h, 'em4', { kind: 'M4' });
+ subscriber(h, 'fresh');
+ armed.freshAfterClose = h.kinds('fresh');
+ armed.cAfterM4 = h.kinds('C');
+ arms[bFirst ? 'bFirst' : 'bSecond'] = armed;
+ }
+ // (i) M3 IS in the buffer C replays: membership was snapshotted at the fan-out's start, so a window that
+ // closes mid-fan-out still counted as a member for that fan-out. Had the close taken effect immediately, C
+ // would have replayed nothing.
+ out.t49l_i_cReplayedM3 = arms.bFirst.C;
+ out.t49l_i_exactlyOnce = arms.bFirst.C.filter(function (k) { return k === 'M3'; }).length === 1;
+ // (ii) Order-independent: the buffer's contents depend on message order and member-open-time, never on the
+ // order in which subscribers happen to run.
+ out.t49l_ii_orderIndependent = JSON.stringify(arms.bFirst.C) === JSON.stringify(arms.bSecond.C);
+ out.t49l_ii_arms = arms;
+ // (iii) C is subscribed by then, so it receives M4 LIVE - what it does not get is a second replay.
+ out.t49l_iii_cGotM4Live = arms.bFirst.cAfterM4;
+ out.t49l_iv_bufferDiscarded = arms.bFirst.freshAfterClose.length === 0
+ && arms.bSecond.freshAfterClose.length === 0;
+ }
+
+ process.stdout.write(JSON.stringify(out));
+})().catch(function (e) {
+ process.stderr.write(String(e && e.stack ? e.stack : e));
+ process.exit(1);
+});
diff --git a/juneau-rest/juneau-rest-server-views/src/test/js/regions-bus.cjs b/juneau-rest/juneau-rest-server-views/src/test/js/regions-bus.cjs
new file mode 100644
index 0000000000..fc0dc7e1ff
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-views/src/test/js/regions-bus.cjs
@@ -0,0 +1,429 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * regions-bus.cjs - always-on Node harness for the region message bus: broadcast-by-default, opt-in targeted
+ * delivery, payload opacity, per-subscriber error isolation, subscription-ordered synchronous delivery with
+ * emit-during-delivery queued under an independent depth cap, unsubscribe both returned and swept at teardown, the
+ * closed set of framework-emitted message schemas, and SD-2's emit-ownership invariant in both directions.
+ *
+ * Usage: node regions-bus.cjs <juneau-renders.js> <juneau-views.js> <juneau-regions.js>
+ */
+'use strict';
+
+const path = require('node:path');
+const H = require(path.join(__dirname, 'regions-harness.cjs'));
+
+const [, , rendersJsPath, viewsJsPath, regionsJsPath] = process.argv;
+if (!rendersJsPath || !viewsJsPath || !regionsJsPath) {
+ console.error('usage: node regions-bus.cjs <juneau-renders.js> <juneau-views.js> <juneau-regions.js>');
+ process.exit(2);
+}
+
+const out = {};
+
+/**
+ * Builds a page of subscriber regions, each recording what it receives. Returns the ctx of each by id, plus the
+ * shared receive log. Everything here uses ONLY ctx.emit / ctx.on - a region never sees the bus itself.
+ */
+function page(h, opts) {
+ const { env, R, rec, clock } = h;
+ const ctxs = {};
+ const received = {};
+ const order = [];
+ R.register('sub', function (ctx) {
+ ctxs[ctx.id] = ctx;
+ received[ctx.id] = [];
+ ctx.on(function (msg, meta) {
+ received[ctx.id].push({ msg: msg, meta: meta });
+ order.push(ctx.id);
+ if (opts && opts.onMessage) opts.onMessage(ctx, msg, meta);
+ });
+ });
+ R.register('silent', function (ctx) { ctxs[ctx.id] = ctx; received[ctx.id] = []; });
+ return { env: env, R: R, rec: rec, clock: clock, ctxs: ctxs, received: received, order: order };
+}
+
+function mk(p, id, opts) {
+ const el = H.mkRegion(p.env, Object.assign({ id: id, type: 'card-body', populate: 'sub' }, opts || {}));
+ p.R.initRegion(el);
+ return el;
+}
+
+function kinds(list) {
+ return list.map(function (r) { return r.msg.kind; });
+}
+
+(async function () {
+
+ // =================================================================================================================
+ // Test 17 - broadcast reaches ALL subscribers but NOT the emitter. A self-reflecting control cannot learn its own
+ // state from the bus and must keep it in local state.
+ // =================================================================================================================
+ {
+ const p = page(H.load(rendersJsPath, viewsJsPath, regionsJsPath));
+ mk(p, 'a'); mk(p, 'b'); mk(p, 'c');
+ p.ctxs.a.emit({ kind: 'ping' });
+ out.t17_a = kinds(p.received.a);
+ out.t17_b = kinds(p.received.b);
+ out.t17_c = kinds(p.received.c);
+ out.t17_metaFrom = p.received.b[0].meta.from;
+ out.t17_metaToNullOnBroadcast = p.received.b[0].meta.to === null;
+ // Synchronous: by the time emit() returns, every subscriber has already run.
+ out.t17_synchronous = p.received.b.length === 1;
+ }
+
+ // =================================================================================================================
+ // Test 18 - targeted delivery reaches EXACTLY one, and a short id and a fully-qualified key both resolve.
+ // Test 19 - an unknown target is a no-op plus a warning, never a throw.
+ // =================================================================================================================
+ {
+ const p = page(H.load(rendersJsPath, viewsJsPath, regionsJsPath));
+ mk(p, 'a', { host: 'grid' }); mk(p, 'b', { host: 'grid' }); mk(p, 'c', { host: 'grid' });
+ // Short id, resolved within the emitter's own host.
+ p.ctxs.a.emit({ kind: 'short' }, { to: 'b' });
+ out.t18_shortB = kinds(p.received.b);
+ out.t18_shortC = kinds(p.received.c);
+ out.t18_metaTo = p.received.b[0].meta.to;
+ // Fully-qualified key.
+ p.ctxs.a.emit({ kind: 'qualified' }, { to: 'grid/c' });
+ out.t18_qualifiedC = kinds(p.received.c);
+ out.t18_qualifiedB = kinds(p.received.b);
+ out.t18_keys = [p.ctxs.a.key, p.ctxs.b.key, p.ctxs.c.key];
+
+ let threw = false;
+ try { p.ctxs.a.emit({ kind: 'nowhere' }, { to: 'does-not-exist' }); } catch (e) { threw = true; }
+ out.t19_noThrow = threw === false;
+ out.t19_warned = p.rec.warnsMatching("no region matches emit target 'does-not-exist'").length === 1;
+ out.t19_nobodyGotIt = kinds(p.received.b).indexOf('nowhere') < 0 && kinds(p.received.c).indexOf('nowhere') < 0;
+
+ // A region addressing ITSELF is refused with a warning rather than delivered.
+ p.ctxs.a.emit({ kind: 'selfie' }, { to: 'a' });
+ out.t19_selfAddressWarned = p.rec.warnsMatching('addressed itself').length === 1;
+ out.t19_selfAddressNotDelivered = kinds(p.received.a).length === 0;
+ }
+
+ // =================================================================================================================
+ // Test 20 - PAYLOAD OPACITY: a getter-trapped payload arrives with ZERO recorded property reads. The framework
+ // moves the object reference and never reads a property of it - no normalizing, no kind-based routing, no cloning,
+ // no serialization - so payload IDENTITY is shared with every subscriber.
+ // =================================================================================================================
+ {
+ const p = page(H.load(rendersJsPath, viewsJsPath, regionsJsPath));
+ mk(p, 'a'); mk(p, 'b'); mk(p, 'c');
+ const reads = [];
+ const raw = { kind: 'trapped', payload: 1 };
+ const trapped = new Proxy(raw, {
+ get: function (t, k) { reads.push(String(k)); return t[k]; },
+ has: function (t, k) { reads.push('has:' + String(k)); return k in t; },
+ ownKeys: function (t) { reads.push('ownKeys'); return Reflect.ownKeys(t); }
+ });
+ p.ctxs.a.emit(trapped);
+ out.t20_reads = reads.slice();
+ out.t20_zeroReads = reads.length === 0;
+ out.t20_identityShared = p.received.b[0].msg === trapped && p.received.c[0].msg === trapped;
+
+ // ...and a TARGETED emit reads nothing either: routing is done from `opts`, never from the payload.
+ reads.length = 0;
+ p.ctxs.a.emit(trapped, { to: 'b' });
+ out.t20_zeroReadsTargeted = reads.length === 0;
+ }
+
+ // =================================================================================================================
+ // Test 21 - a THROWING subscriber does not block the rest of the fan-out, and is reported. A repeatedly throwing
+ // subscriber is deliberately NOT auto-unsubscribed: silently dropping it turns a loud bug into a mysterious one.
+ // =================================================================================================================
+ {
+ const h = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ const p = page(h, {
+ onMessage: function (ctx) { if (ctx.id === 'b') throw new Error('subscriber exploded'); }
+ });
+ mk(p, 'a'); mk(p, 'b'); mk(p, 'c');
+ p.ctxs.a.emit({ kind: 'm1' });
+ out.t21_cStillGotIt = kinds(p.received.c);
+ out.t21_reported = p.rec.errorsMatching("subscriber 'b' threw handling a message from 'a'").length === 1;
+ // Not auto-unsubscribed: it throws again on the next message, and c still receives that one too.
+ p.ctxs.a.emit({ kind: 'm2' });
+ out.t21_stillSubscribed = kinds(p.received.b);
+ out.t21_cGotBoth = kinds(p.received.c);
+ out.t21_reportedTwice = p.rec.errorsMatching("subscriber 'b' threw").length === 2;
+ }
+
+ // =================================================================================================================
+ // Test 22 - delivery order is SUBSCRIPTION order.
+ // =================================================================================================================
+ {
+ const p = page(H.load(rendersJsPath, viewsJsPath, regionsJsPath));
+ mk(p, 'z'); mk(p, 'm'); mk(p, 'a'); mk(p, 'driver');
+ p.ctxs.driver.emit({ kind: 'ordered' });
+ out.t22_order = p.order.slice();
+ }
+
+ // =================================================================================================================
+ // Test 23 - an emit DURING delivery is QUEUED, not nested, asserted by CALL-ORDER RECORDING rather than by
+ // absence-of-crash. A control card that emits its initial filter state during its own paint cannot re-enter a
+ // listener mid-paint.
+ // =================================================================================================================
+ {
+ const trace = [];
+ const h = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ const p = page(h, {
+ onMessage: function (ctx, msg) {
+ trace.push('enter:' + ctx.id + ':' + msg.kind);
+ if (ctx.id === 'b' && msg.kind === 'm1') ctx.emit({ kind: 'm2' });
+ trace.push('exit:' + ctx.id + ':' + msg.kind);
+ }
+ });
+ mk(p, 'b'); mk(p, 'c'); mk(p, 'driver');
+ p.ctxs.driver.emit({ kind: 'm1' });
+ out.t23_trace = trace.slice();
+ // The nested-delivery signature would be enter:b:m1, enter:c:m2, exit:c:m2, exit:b:m1. The queued signature
+ // is every m1 handler completing before any m2 handler starts.
+ out.t23_notNested = trace.indexOf('exit:b:m1') < trace.indexOf('enter:c:m2');
+ out.t23_m1FanOutCompletedFirst = trace.indexOf('enter:c:m1') < trace.indexOf('enter:c:m2');
+ out.t23_cGot = kinds(p.received.c);
+ }
+
+ // =================================================================================================================
+ // Test 24 - an emit cycle hits the depth cap and errors loudly, PRINTING THE CYCLE - the shortest repeating
+ // (from -> to) suffix - not an arbitrary pair of keys. Asserted with a THREE-region cycle as well as a two-region
+ // one, since the two-region case cannot distinguish "prints the cycle" from "prints the last two hops".
+ // =================================================================================================================
+ for (const n of [2, 3]) {
+ const ring = n === 2 ? { a: 'b', b: 'a' } : { a: 'b', b: 'c', c: 'a' };
+ const h = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ const p = page(h, {
+ onMessage: function (ctx) { ctx.emit({ kind: 'loop' }, { to: ring[ctx.id] }); }
+ });
+ for (const id of Object.keys(ring)) mk(p, id);
+ p.ctxs.a.emit({ kind: 'loop' }, { to: ring.a });
+ const errs = p.rec.errorsMatching('emit cycle detected');
+ out['t24_' + n + '_exactlyOneError'] = errs.length === 1;
+ out['t24_' + n + '_terminated'] = true; // reaching this line at all means the drain terminated
+ const cycle = errs.length ? errs[0].slice(errs[0].indexOf('Cycle: ') + 7) : '';
+ out['t24_' + n + '_cycle'] = cycle;
+ out['t24_' + n + '_hopCount'] = cycle.split('|').length;
+ out['t24_' + n + '_namesEveryRegion'] = Object.keys(ring).every(function (id) { return cycle.indexOf(id) >= 0; });
+ }
+
+ // =================================================================================================================
+ // Test 25 - UNSUBSCRIBE-ON-TEARDOWN, the leak test: a torn-down region receives nothing even though its handler is
+ // still reachable from the test's own closure.
+ // Test 26 - ctx.on's returned unsubscribe works independently of teardown.
+ // =================================================================================================================
+ {
+ const p = page(H.load(rendersJsPath, viewsJsPath, regionsJsPath));
+ const torn = mk(p, 'torn');
+ mk(p, 'kept');
+ mk(p, 'driver');
+ const handler = p.received.torn; // the test's closure still holds the array the handler pushes into
+ p.ctxs.driver.emit({ kind: 'before' });
+ out.t25_beforeTeardown = kinds(handler);
+ p.R.teardownRegion(torn._juneauRegion);
+ p.ctxs.driver.emit({ kind: 'after' });
+ out.t25_afterTeardown = kinds(handler);
+ out.t25_siblingUnaffected = kinds(p.received.kept);
+
+ // The returned unsubscribe is independent of teardown.
+ let got = [];
+ let off = null;
+ p.R.register('selfmanaged', function (ctx) { off = ctx.on(function (m) { got.push(m.kind); }); });
+ const sm = H.mkRegion(p.env, { id: 'sm', type: 'card-body', populate: 'selfmanaged' });
+ p.R.initRegion(sm);
+ p.ctxs.driver.emit({ kind: 'one' });
+ off();
+ p.ctxs.driver.emit({ kind: 'two' });
+ out.t26_receivedBeforeOff = got.slice();
+ out.t26_regionStillLive = sm._juneauRegion !== undefined;
+ out.t26_stateStillOk = sm.getAttribute('data-juneau-region-state');
+ }
+
+ // =================================================================================================================
+ // Teardown step 4 - a message a region QUEUED but that has not been delivered yet is DISCARDED: a region that is
+ // going away should not drive the page after it is gone.
+ // =================================================================================================================
+ {
+ const h = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ let toTearDown = null;
+ const p = page(h, {
+ onMessage: function (ctx, msg) {
+ if (ctx.id === 'b' && msg.kind === 'm1') ctx.emit({ kind: 'from-b' });
+ if (ctx.id === 'a' && msg.kind === 'm1') p.R.teardownRegion(toTearDown._juneauRegion);
+ }
+ });
+ toTearDown = mk(p, 'b'); // subscribes FIRST, so it queues before a tears it down in the same fan-out
+ mk(p, 'a');
+ mk(p, 'c');
+ mk(p, 'driver');
+ p.ctxs.driver.emit({ kind: 'm1' });
+ out.t_step4_cGot = kinds(p.received.c);
+ out.t_step4_queuedMessageDropped = kinds(p.received.c).indexOf('from-b') < 0;
+ }
+
+ // =================================================================================================================
+ // Test 27 / 27a / 27c / 51 - the framework-emitted set is SMALL and CLOSED: selection changed, detail toggled,
+ // table redrew, and nothing else. 27a/27c are the CHROME half of SD-2's ownership invariant: a region beside the
+ // chrome is driven with NO author emit code at all - only a ctx.on plus a msg.viewId filter.
+ // =================================================================================================================
+ {
+ const h = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ const R = h.R;
+ const seen = [];
+ let mine = [];
+ R.register('chart', function (ctx) {
+ // The entire consumer-side contract for a chrome-driven region: subscribe, filter on viewId. No emit.
+ ctx.on(function (msg) {
+ seen.push(msg);
+ if (msg.viewId === 'gacks') mine.push(msg.kind);
+ });
+ });
+ R.initRegion(H.mkRegion(h.env, { id: 'chart', type: 'card-body', populate: 'chart' }));
+
+ // 27a - selection changed. Emitted BY THE FRAMEWORK, so it reaches every subscriber including one whose own
+ // chrome caused it: meta.from names the framework rather than a region.
+ R.emitFramework(R.selectionChangedMessage({ viewId: 'gacks', ids: ['42'], rows: [{ id: '42' }], added: ['42'], removed: [] }));
+ // 27c - detail expand/collapse.
+ R.emitFramework(R.detailToggledMessage({ viewId: 'gacks', rowId: '42', expanded: true, generation: 7 }));
+ // table redrew.
+ R.emitFramework(R.tableRedrewMessage({ viewId: 'gacks', nested: false, rowCount: 25, page: 0 }));
+
+ out.t27_kinds = seen.map(function (m) { return m.kind; });
+ out.t27_allNamespaced = seen.every(function (m) { return m.kind.indexOf('juneau:') === 0; });
+ out.t27_senderIsFramework = R.FRAMEWORK_SENDER_KEY;
+ out.t27a_drivenWithNoAuthorEmit = mine.slice();
+
+ // 51 - a golden per framework message: kind including the prefix, schemaVersion, and the full field set.
+ out.t51_selectionKeys = Object.keys(seen[0]).sort();
+ out.t51_selection = seen[0];
+ out.t51_detailKeys = Object.keys(seen[1]).sort();
+ out.t51_detail = seen[1];
+ out.t51_redrewKeys = Object.keys(seen[2]).sort();
+ out.t51_redrew = seen[2];
+ out.t51_schemaVersionsIndependentOfContract = seen.every(function (m) { return m.schemaVersion === 1; })
+ && R.CONTRACT_VERSION === '1';
+
+ // 51 - the DISAMBIGUATION case: two tables on one page, each emitting selection, and a subscriber correctly
+ // filtering on msg.viewId. Without viewId on the payload this is unanswerable, because meta.from names the
+ // framework and there is no host for a short target to resolve against.
+ mine = [];
+ R.emitFramework(R.selectionChangedMessage({ viewId: 'gacks', ids: ['1'] }));
+ R.emitFramework(R.selectionChangedMessage({ viewId: 'other', ids: ['2'] }));
+ out.t51_filteredByViewId = mine.length === 1;
+ out.t51_viewIdsSeen = seen.slice(3).map(function (m) { return m.viewId; });
+ }
+
+ // =================================================================================================================
+ // Test 27b - CONTENT DOES NOT AUTO-EMIT (SD-2 / R25b). The negative test, and the one that keeps someone from
+ // later adding a "helpful" click delegate over author DOM and quietly breaking the ownership invariant. A
+ // populate paints clickable elements, calls no emit, and clicking them produces ZERO bus traffic.
+ // =================================================================================================================
+ {
+ const h = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ const p = page(h);
+ mk(p, 'watcher');
+ const buttons = [];
+ h.R.register('painter', function (ctx, container) {
+ for (const label of ['one', 'two', 'three']) {
+ const b = h.env.el('button');
+ b.textContent = label;
+ b.setAttribute('data-author-action', label);
+ // The author's OWN listener - the framework must not add one of its own beside it.
+ b.addEventListener('click', function () { b.setAttribute('data-clicked', '1'); });
+ container.appendChild(b);
+ buttons.push(b);
+ }
+ });
+ const el = H.mkRegion(h.env, { id: 'content', type: 'card-body', populate: 'painter' });
+ h.R.initRegion(el);
+
+ for (const b of buttons) b.dispatch('click');
+ out.t27b_authorHandlersRan = buttons.every(function (b) { return b.getAttribute('data-clicked') === '1'; });
+ out.t27b_zeroBusTraffic = p.received.watcher.length === 0;
+ // The framework bound NO listener of its own inside the region container: every listener on every painted
+ // node is the author's single click handler.
+ out.t27b_listenerCounts = buttons.map(function (b) { return Object.keys(b._listeners).map(function (k) { return k + ':' + b._listeners[k].length; }).join(','); });
+ out.t27b_containerHasNoFrameworkListener = Object.keys(el._listeners).length === 0;
+ // ...and the region container itself never gained a delegate on the way in or out.
+ el.dispatch('click');
+ out.t27b_stillZeroAfterContainerClick = p.received.watcher.length === 0;
+ }
+
+ // =================================================================================================================
+ // Test 50 - OUT-OF-ORDER SETTLE. Two rapid messages to an update-in-place subscriber, the first response
+ // artificially delayed past the second: the container holds the SECOND message's data and the first fetch's signal
+ // was aborted. Written with deterministic promise ordering, not timers. The handler uses ctx.signal.fork() and
+ // never the bare ctx.signal - the bare signal is aborted by nothing in this mode, so an unbounded number of
+ // fetches would share one signal and the last to settle would win the paint.
+ // =================================================================================================================
+ {
+ const h = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ const R = h.R;
+ let container = null;
+ const pending = H.abortableFetch(h.env);
+ const signals = pending;
+ // The page's own fetch, so the harness sees what the populate issued. A region author writes plain
+ // `fetch(url, {signal: sig})`; only the indirection differs here.
+ const pageFetch = function (url, init) { return h.env.callFetch(url, init); };
+ R.register('inplace', function (ctx, el) {
+ container = el;
+ ctx.on(function (msg) {
+ const sig = ctx.signal.fork();
+ pageFetch('/probe/' + msg.probe, { signal: sig })
+ .then(function (res) { return res.text(); })
+ .then(function (text) { el.textContent = text; })
+ .catch(function (e) { if (e.name !== 'AbortError') throw e; });
+ });
+ });
+ let driver = null;
+ R.register('driver', function (ctx) { driver = ctx; });
+ R.initRegion(H.mkRegion(h.env, { id: 'ip', type: 'card-body', populate: 'inplace' }));
+ R.initRegion(H.mkRegion(h.env, { id: 'drv', type: 'card-body', populate: 'driver' }));
+
+ driver.emit({ kind: 'probe-selected', probe: 1 });
+ driver.emit({ kind: 'probe-selected', probe: 2 });
+ out.t50_twoFetches = pending.length === 2;
+ out.t50_firstSignalAborted = signals[0].init.signal.aborted === true;
+ out.t50_secondSignalLive = signals[1].init.signal.aborted === false;
+ // Settle in the WRONG order: the second response first, then the stale first one - which the abort has
+ // already made unsettleable, which is the whole point of forking.
+ out.t50_secondSettled = pending.resolveText(1, 'probe-2');
+ await H.flush();
+ out.t50_staleFirstCannotSettle = pending.resolveText(0, 'probe-1') === false;
+ await H.flush();
+ out.t50_containerHoldsSecond = container.textContent;
+ out.t50_noRegionError = h.rec.errors.length === 0;
+
+ // Three rapid clicks with responses returning in the order 3, 1, 2 - the flagship shape. The panel holds the
+ // LAST REQUESTED probe, the two superseded signals are aborted, and no AbortError ever surfaced as a region
+ // error (the calendar-idiom swallow).
+ driver.emit({ kind: 'probe-selected', probe: 3 });
+ driver.emit({ kind: 'probe-selected', probe: 4 });
+ driver.emit({ kind: 'probe-selected', probe: 5 });
+ out.t50a_lastSettles = pending.resolveText(4, 'probe-5');
+ await H.flush();
+ out.t50a_supersededCannotSettle = [pending.resolveText(2, 'probe-3'), pending.resolveText(3, 'probe-4')];
+ await H.flush();
+ out.t50a_holdsLast = container.textContent;
+ out.t50a_earlierSignalsAborted = [2, 3, 4].map(function (i) { return signals[i].init.signal.aborted; });
+ out.t50a_noAbortErrorSurfaced = h.rec.errors.length === 0;
+ }
+
+ process.stdout.write(JSON.stringify(out));
+})().catch(function (e) {
+ process.stderr.write(String(e && e.stack ? e.stack : e));
+ process.exit(1);
+});
diff --git a/juneau-rest/juneau-rest-server-views/src/test/js/regions-harness.cjs b/juneau-rest/juneau-rest-server-views/src/test/js/regions-harness.cjs
new file mode 100644
index 0000000000..146f3b84c8
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-views/src/test/js/regions-harness.cjs
@@ -0,0 +1,251 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * regions-harness.cjs - the shared loader for the always-on Node behavioral harnesses that exercise
+ * juneau-regions.js: the populate primitive, the message bus, SD-2's emit-ownership invariant, and the
+ * initial-broadcast barrier.
+ *
+ * It layers three things on top of views-dom-shim.cjs, each of which a region test cannot do without:
+ *
+ * 1. A CONTROLLABLE CLOCK. The shim's own loadViews() sandbox runs setTimeout callbacks IMMEDIATELY and makes
+ * clearTimeout a no-op, which is fine for the runtimes it was written for and useless here: the barrier's
+ * whole contract is "drains at 0ms when every region settled, drains at the 2000ms deadline when one did
+ * not", and asserting that needs the clock to be an input rather than a fact. Several assertions in the
+ * design's test list are explicitly against the clock rather than against delivery order, because delivery
+ * is correct under both readings and only the latency differs.
+ * 2. A RECORDING CONSOLE, because "exactly one error naming X" is an assertion in five of these tests.
+ * 3. AN INJECTABLE AbortController, so the baseline-fails-loud test can load a page into an environment that
+ * genuinely does not have one - a `delete globalThis.AbortController` in-process would not be observable
+ * by an already-loaded runtime, since the runtime feature-tests exactly once at load.
+ *
+ * const { load, mkRegion, flush } = require('./regions-harness.cjs');
+ */
+'use strict';
+
+const fs = require('node:fs');
+const path = require('node:path');
+const vm = require('node:vm');
+
+const { makeEnv, jsonResponse } = require(path.join(__dirname, 'views-dom-shim.cjs'));
+
+/**
+ * A fake macrotask clock. `advance(ms)` fires every timer due at or before the new time, in due-time order and
+ * then in scheduling order, which is what makes "the drain happened at 0ms" and "the drain happened at 2000ms"
+ * two distinguishable outcomes.
+ */
+function makeClock() {
+ let now = 0;
+ let seq = 0;
+ let timers = [];
+ return {
+ now: function () { return now; },
+ pending: function () { return timers.length; },
+ set: function (fn, ms) {
+ const id = ++seq;
+ timers.push({ id: id, at: now + (ms || 0), seq: id, fn: fn });
+ return id;
+ },
+ clear: function (id) { timers = timers.filter(function (t) { return t.id !== id; }); },
+ /** Fires everything due within `ms` of now. `advance(0)` fires the zero-delay macrotasks and nothing else. */
+ advance: function (ms) {
+ const until = now + (ms || 0);
+ for (;;) {
+ const due = timers.filter(function (t) { return t.at <= until; });
+ if (due.length === 0) break;
+ due.sort(function (a, b) { return a.at - b.at || a.seq - b.seq; });
+ const t = due[0];
+ timers = timers.filter(function (x) { return x !== t; });
+ now = Math.max(now, t.at);
+ t.fn();
+ }
+ now = until;
+ }
+ };
+}
+
+/** A console that records instead of printing, so "exactly one error naming X" is assertable. */
+function makeConsole() {
+ const rec = { errors: [], warns: [], logs: [] };
+ rec.console = {
+ error: function (...a) { rec.errors.push(a.map(String).join(' ')); },
+ warn: function (...a) { rec.warns.push(a.map(String).join(' ')); },
+ log: function (...a) { rec.logs.push(a.map(String).join(' ')); },
+ info: function (...a) { rec.logs.push(a.map(String).join(' ')); },
+ debug: function (...a) { rec.logs.push(a.map(String).join(' ')); }
+ };
+ rec.errorsMatching = function (needle) {
+ return rec.errors.filter(function (e) { return e.indexOf(needle) >= 0; });
+ };
+ rec.warnsMatching = function (needle) {
+ return rec.warns.filter(function (e) { return e.indexOf(needle) >= 0; });
+ };
+ return rec;
+}
+
+/**
+ * Loads juneau-renders.js, juneau-views.js and juneau-regions.js into one fresh environment.
+ *
+ * `opts.noAbortController` withholds the platform baseline for the fail-loud test.
+ */
+function load(rendersJsPath, viewsJsPath, regionsJsPath, opts) {
+ opts = opts || {};
+ const env = makeEnv();
+ const rec = makeConsole();
+ const clock = makeClock();
+
+ env.window.console = rec.console;
+ env.window.setTimeout = clock.set;
+ env.window.clearTimeout = clock.clear;
+
+ const sandbox = {
+ window: env.window,
+ document: env.document,
+ console: rec.console,
+ setTimeout: clock.set,
+ clearTimeout: clock.clear,
+ setInterval: function () { return 0; },
+ clearInterval: function () {},
+ Promise: Promise,
+ fetch: function (...args) { return env.callFetch(...args); }
+ };
+ if (!opts.noAbortController) {
+ sandbox.AbortController = AbortController;
+ sandbox.AbortSignal = AbortSignal;
+ sandbox.DOMException = DOMException;
+ }
+
+ for (const file of [rendersJsPath, viewsJsPath, regionsJsPath]) {
+ // NOSONAR javascript:S1523 -- loading the production juneau-renders.js/juneau-views.js/juneau-regions.js
+ // sources into a VM sandbox is this harness's intended mechanism for exercising them under the DOM shim;
+ // inputs are fixed local file paths supplied by the test, never attacker-controlled data.
+ vm.runInNewContext(fs.readFileSync(path.resolve(file), 'utf8'), sandbox, { filename: path.basename(file) });
+ }
+
+ const NS = env.window.JuneauViews;
+ return { env: env, NS: NS, R: NS?.regions, I: NS?.init, rec: rec, clock: clock };
+}
+
+/**
+ * Builds a region container and appends it (default: to `body`). `opts.hiddenPanel` wraps it in a non-active
+ * `.jc-panel`, which is the live shape a card in a non-initial tab has and the one the runtime's own visibility
+ * predicate reports as hidden.
+ */
+function mkRegion(env, opts) {
+ const el = env.el('div');
+ el.setAttribute('data-juneau-region', opts.id);
+ if (opts.type) el.setAttribute('data-juneau-region-type', opts.type);
+ if (opts.host) el.setAttribute('data-juneau-region-host', opts.host);
+ if (opts.populate) el.setAttribute('data-juneau-region-populate', opts.populate);
+ let parent = opts.parent || env.body;
+ if (opts.hiddenPanel) {
+ const panel = env.el('div');
+ panel.className = 'jc-panel';
+ parent.appendChild(panel);
+ el.__panel = panel;
+ parent = panel;
+ }
+ parent.appendChild(el);
+ return el;
+}
+
+/** Marks a `.jc-panel` wrapper active, so the region inside it becomes visible. */
+function activatePanel(el) {
+ if (el.__panel) el.__panel.className = 'jc-panel jc-active';
+}
+
+/** Drains the real microtask queue `n` times, so a chain of promise continuations has all run. */
+async function flush(n) {
+ for (let i = 0; i < (n || 6); i++) await Promise.resolve();
+}
+
+/**
+ * Installs a fetch that HONORS ITS SIGNAL, and returns the live call log.
+ *
+ * The fidelity matters more than it looks: a fake that resolves whatever the test resolves, signal or no signal, lets
+ * a stale response overwrite a fresh one and makes the out-of-order-settle tests pass for the wrong reason - they
+ * would be asserting the test's own resolve order rather than the runtime's cancellation. A real fetch rejects with
+ * `signal.reason` the moment the signal aborts and ignores the response that arrives afterwards, so this one does
+ * too: `resolve(entry, body)` on an already-aborted call is a no-op, exactly as a socket read on a cancelled request
+ * is.
+ */
+function abortableFetch(env) {
+ const calls = [];
+ env.setFetch(function (url, init) {
+ const d = deferred();
+ const entry = { url: url, init: init, aborted: false, settled: false, _d: d };
+ const abort = function () {
+ entry.aborted = true;
+ entry.settled = true;
+ d.reject(init.signal.reason);
+ };
+ calls.push(entry);
+ if (init?.signal) {
+ if (init.signal.aborted) abort();
+ else init.signal.addEventListener('abort', abort, { once: true });
+ }
+ return d.promise;
+ });
+ calls.resolve = function (i, body, opts) {
+ const entry = calls[i];
+ if (entry.settled) return false;
+ entry.settled = true;
+ entry._d.resolve(jsonResponse(body, opts));
+ return true;
+ };
+ calls.resolveText = function (i, text) {
+ const entry = calls[i];
+ if (entry.settled) return false;
+ entry.settled = true;
+ entry._d.resolve({ ok: true, status: 200, text: function () { return Promise.resolve(text); } });
+ return true;
+ };
+ return calls;
+}
+
+/** A deferred whose promise settles only when the test says so. */
+function deferred() {
+ let resolve, reject;
+ const promise = new Promise(function (res, rej) { resolve = res; reject = rej; });
+ return { promise: promise, resolve: resolve, reject: reject };
+}
+
+/**
+ * A thenable whose `then` is SYNCHRONOUS: calling `settle()` runs the framework's continuation in the caller's own
+ * turn rather than on a microtask. This is the only construction that can make a released region's populate settle
+ * partway through a fan-out, which is the last-window-close case; resolving an ordinary promise from inside a
+ * subscriber queues a microtask, so the continuation runs after the synchronous drain has already finished and the
+ * assertion silently tests the ordinary case instead.
+ */
+function syncThenable() {
+ const box = { settle: null };
+ box.thenable = {
+ then: function (res) { box.settle = function (v) { res(v); }; }
+ };
+ return box;
+}
+
+module.exports = {
+ load: load,
+ mkRegion: mkRegion,
+ activatePanel: activatePanel,
+ flush: flush,
+ deferred: deferred,
+ abortableFetch: abortableFetch,
+ syncThenable: syncThenable,
+ jsonResponse: jsonResponse
+};
diff --git a/juneau-rest/juneau-rest-server-views/src/test/js/regions-primitive.cjs b/juneau-rest/juneau-rest-server-views/src/test/js/regions-primitive.cjs
new file mode 100644
index 0000000000..84786eaf8c
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-views/src/test/js/regions-primitive.cjs
@@ -0,0 +1,595 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * regions-primitive.cjs - always-on Node harness for the populate(ctx, container) primitive: the ctx golden, the
+ * clear-before-every-call and coalesce-on-in-flight idempotency rules, contained throws, the four return-value
+ * shapes, the fixed teardown order, cancellation generations including signal.fork()'s attachment mechanics, the
+ * AbortController baseline refusal, and reason-tracks-the-causal-event.
+ *
+ * Usage: node regions-primitive.cjs <juneau-renders.js> <juneau-views.js> <juneau-regions.js>
+ */
+'use strict';
+
+const path = require('node:path');
+const H = require(path.join(__dirname, 'regions-harness.cjs'));
+
+const [, , rendersJsPath, viewsJsPath, regionsJsPath] = process.argv;
+if (!rendersJsPath || !viewsJsPath || !regionsJsPath) {
+ console.error('usage: node regions-primitive.cjs <juneau-renders.js> <juneau-views.js> <juneau-regions.js>');
+ process.exit(2);
+}
+
+const out = {};
+
+function stateOf(el) {
+ return el.getAttribute('data-juneau-region-state');
+}
+
+(async function () {
+
+ // =================================================================================================================
+ // Test 1 - the container is EMPTY and IN-DOCUMENT at the first call, for each of the three region types.
+ // =================================================================================================================
+ {
+ const { env, R } = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ const seen = {};
+ R.register('probe', function (ctx, container) {
+ seen[ctx.type] = {
+ childCount: container.childNodes.length,
+ connected: env.document.contains(container),
+ sameNode: container === ctx0[ctx.type],
+ state: stateOf(container)
+ };
+ container.appendChild(env.el('span'));
+ });
+ const ctx0 = {};
+ for (const type of ['row-detail', 'card-body', 'tab-body']) {
+ const el = H.mkRegion(env, { id: 'r-' + type, type: type, populate: 'probe' });
+ ctx0[type] = el;
+ R.initRegion(el);
+ }
+ out.t1_types = Object.keys(seen).sort();
+ out.t1_allEmpty = Object.values(seen).every(function (s) { return s.childCount === 0; });
+ out.t1_allConnected = Object.values(seen).every(function (s) { return s.connected === true; });
+ out.t1_allSameNode = Object.values(seen).every(function (s) { return s.sameNode === true; });
+ // The state was already `loading` when the populator ran, and NOTHING had been painted into the container to
+ // say so - the state rides an attribute precisely so the container can stay empty.
+ out.t1_loadingViaAttrNotDom = Object.values(seen).every(function (s) { return s.state === 'loading'; });
+ }
+
+ // =================================================================================================================
+ // Test 2 - the ctx field set is an EXACT golden, and fork()'s attachment does not show up in it.
+ // =================================================================================================================
+ {
+ const { env, R } = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ let ctx = null;
+ R.register('capture', function (c) { ctx = c; });
+ const grid = env.el('div');
+ grid.setAttribute('data-juneau-card-grid', 'glance');
+ const card = env.el('div');
+ card.setAttribute('data-juneau-card', 'posture');
+ grid.appendChild(card);
+ env.body.appendChild(grid);
+ const el = H.mkRegion(env, { id: 'diagnose', type: 'row-detail', host: 'gacks/row-42', populate: 'capture', parent: card });
+ R.initRegion(el);
+
+ out.t2_keys = Object.keys(ctx).sort();
+ out.t2_declaredKeys = Object.keys(ctx.declared).sort();
+ out.t2_idsKeys = Object.keys(ctx.ids).sort();
+ out.t2_types = {};
+ for (const k of Object.keys(ctx).sort()) out.t2_types[k] = typeof ctx[k];
+ out.t2_contractVersion = ctx.contractVersion;
+ out.t2_type = ctx.type;
+ out.t2_id = ctx.id;
+ out.t2_key = ctx.key;
+ out.t2_host = ctx.host;
+ out.t2_reason = ctx.reason;
+ out.t2_generation = ctx.generation;
+ out.t2_messageSignalNullOnInitial = ctx.messageSignal === null;
+ out.t2_dataNull = ctx.data === null;
+ out.t2_selectionNull = ctx.selection === null;
+ out.t2_paramsIsObject = ctx.params !== null && typeof ctx.params === 'object';
+ // ids is a flat bag: it carries what the framework already knew from the ancestry, and null elsewhere.
+ out.t2_idsGrid = ctx.ids.gridId;
+ out.t2_idsCard = ctx.ids.cardId;
+ out.t2_idsView = ctx.ids.viewId;
+ // SD-3: sectionId is on the shape and is null on a row-detail region. A member whose value is null for one
+ // region type is not a member that left.
+ out.t2_idsSectionNullOnRowDetail = ctx.ids.sectionId === null;
+ // declared.fields is a member of the contract here even though nothing populates it yet.
+ out.t2_declaredFieldsNull = ctx.declared.fields === null;
+ out.t2_declaredTitleFieldsNull = ctx.declared.titleFields === null;
+ }
+
+ // =================================================================================================================
+ // Test 8a - fork() attachment mechanics: six assertions, because `fork` is a non-standard method on a standard
+ // type and every one of these is a way to get it subtly wrong.
+ // =================================================================================================================
+ {
+ const suffix = '';
+ const { env, R } = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ let ctx = null;
+ R.register('capture', function (c) { ctx = c; });
+ const el = H.mkRegion(env, { id: 'forker', type: 'card-body', populate: 'capture' });
+ R.initRegion(el);
+
+ // (i) It is a GENUINE platform signal, not a look-alike: instanceof holds, its own members behave normally,
+ // and the real global fetch accepts it with no adapter.
+ out['t8a_isRealSignal' + suffix] = ctx.signal instanceof AbortSignal;
+ out['t8a_signalMembers' + suffix] = ctx.signal.aborted === false
+ && typeof ctx.signal.addEventListener === 'function'
+ && typeof ctx.signal.throwIfAborted === 'function';
+ // Handed to the platform's OWN fetch, pre-aborted, so the assertion is "fetch consumed it as a signal" with
+ // no socket opened: a look-alike rejects with TypeError, a genuine signal with AbortError.
+ out['t8a_acceptedByRealFetch' + suffix] = await (async function () {
+ const probe = ctx.signal.fork();
+ ctx.signal.fork(); // supersedes `probe`, so it is aborted without tearing the region down
+ try { await fetch('http://juneau.invalid/none', { signal: probe }); return 'RESOLVED'; }
+ catch (e) { return e.name === 'AbortError'; }
+ })();
+ out['t8a_forkTypeof' + suffix] = typeof ctx.signal.fork;
+
+ // (ii) `fork` is an OWN, non-enumerable, non-writable property of that instance.
+ const d = Object.getOwnPropertyDescriptor(ctx.signal, 'fork');
+ out['t8a_own' + suffix] = !!d;
+ out['t8a_enumerable' + suffix] = d ? d.enumerable : null;
+ out['t8a_writable' + suffix] = d ? d.writable : null;
+ out['t8a_notInOwnKeys' + suffix] = Object.keys(ctx.signal).indexOf('fork') < 0;
+
+ // (iii) AbortSignal.prototype.fork is undefined IN A PAGE THAT HAS LOADED THE REGION RUNTIME. The lazy way
+ // to implement fork() is exactly the prototype patch, which is why this is worth a test.
+ out['t8a_notOnPrototype' + suffix] = AbortSignal.prototype.fork === undefined
+ && Object.getOwnPropertyDescriptor(AbortSignal.prototype, 'fork') === undefined;
+
+ // (iv) A forked child is a plain AbortSignal with NO fork of its own - forking is a capability of the
+ // invocation signal, not a chainable operation, so the tree stays exactly two levels deep.
+ const g0 = ctx.generation;
+ const sig1 = ctx.signal.fork();
+ out['t8a_childIsSignal' + suffix] = sig1 instanceof AbortSignal;
+ out['t8a_childHasNoFork' + suffix] = sig1.fork === undefined;
+
+ // (vi) Forking again aborts the first child and increments ctx.generation.
+ const sig2 = ctx.signal.fork();
+ out['t8a_firstChildAborted' + suffix] = sig1.aborted === true;
+ out['t8a_secondChildLive' + suffix] = sig2.aborted === false;
+ out['t8a_generationIncremented' + suffix] = ctx.generation === g0 + 2;
+
+ // (v) Aborting the parent aborts every live child.
+ R.teardownRegion(el._juneauRegion);
+ out['t8a_parentAbortKillsChild' + suffix] = sig2.aborted === true;
+ }
+
+ // =================================================================================================================
+ // Test 3 - idempotent re-populate: two calls yield ONE copy of the content, not two.
+ // =================================================================================================================
+ {
+ const { env, R } = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ let calls = 0;
+ let ctx = null;
+ R.register('once', function (c, container) {
+ calls++;
+ ctx = c;
+ container.appendChild(env.el('span'));
+ });
+ const el = H.mkRegion(env, { id: 'idem', type: 'card-body', populate: 'once' });
+ const handle = R.initRegion(el);
+ // A second enrolment walk over the same node enrols NOTHING new and returns the same handle.
+ const handle2 = R.initRegion(el);
+ out.t3_sameHandle = handle === handle2;
+ out.t3_callsAfterSecondInit = calls;
+ // A re-populate clears first, so the content is one copy and not two.
+ ctx.refresh();
+ await H.flush();
+ out.t3_callsAfterRefresh = calls;
+ out.t3_spanCount = el.querySelectorAll('span').length;
+ }
+
+ // =================================================================================================================
+ // Test 4 - a throwing populate is CONTAINED: the region errors, its siblings stay healthy, the page is alive.
+ // =================================================================================================================
+ {
+ const { env, R, rec } = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ R.register('boom', function () { throw new Error('populate exploded'); });
+ R.register('fine', function (ctx, container) { container.appendChild(env.el('b')); });
+ const bad = H.mkRegion(env, { id: 'bad', type: 'card-body', populate: 'boom' });
+ const good = H.mkRegion(env, { id: 'good', type: 'card-body', populate: 'fine' });
+ R.initRegion(bad);
+ R.initRegion(good);
+ out.t4_badState = stateOf(bad);
+ out.t4_goodState = stateOf(good);
+ out.t4_goodHasContent = good.querySelectorAll('b').length === 1;
+ out.t4_errorNamesRegion = rec.errorsMatching("region 'bad'").length === 1;
+ out.t4_errorVisibleInContainer = (bad.textContent || '').indexOf('populate exploded') >= 0;
+
+ // An unknown populator name fails VISIBLY too - a region has no raw value to fall back to, so a blank
+ // container with no error would read as "no data" rather than "broken page".
+ const unknown = H.mkRegion(env, { id: 'nope', type: 'card-body', populate: 'no-such-populator' });
+ R.initRegion(unknown);
+ out.t4_unknownState = stateOf(unknown);
+ out.t4_unknownErrorNamesName = rec.errorsMatching("no populator is registered under the name 'no-such-populator'").length === 1;
+
+ // The reserved default cannot be overridden, and the refusal mutates nothing.
+ const before = R.resolve('default');
+ const refused = R.register('default', function () {});
+ out.t4_reservedRefused = refused === null && R.resolve('default') === before;
+ out.t4_reservedErrorLogged = rec.errorsMatching('reserved populator name').length === 1;
+ }
+
+ // =================================================================================================================
+ // Test 5 - a Promise-returning populate goes loading -> ok only AFTER the settle.
+ // Test 6a - Promise<() => void>: the cleanup resolved from the promise is invoked exactly once at teardown.
+ // =================================================================================================================
+ {
+ const { env, R } = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ const d = H.deferred();
+ let cleanups = 0;
+ R.register('slow', function (ctx, container) {
+ container.appendChild(env.el('span'));
+ return d.promise.then(function () { return function () { cleanups++; }; });
+ });
+ const el = H.mkRegion(env, { id: 'slow', type: 'tab-body', populate: 'slow' });
+ R.initRegion(el);
+ out.t5_stateWhileInFlight = stateOf(el);
+ d.resolve();
+ await H.flush();
+ out.t5_stateAfterSettle = stateOf(el);
+ out.t6a_cleanupsBeforeTeardown = cleanups;
+ R.teardownRegion(el._juneauRegion);
+ out.t6a_cleanupsAfterTeardown = cleanups;
+ R.teardownRegion(el._juneauRegion);
+ out.t6a_cleanupsAfterSecondTeardown = cleanups;
+
+ // A REJECTED promise is a contained throw and registers NO cleanup: a populate that failed before it finished
+ // allocating cannot be trusted to know what to release.
+ let rejectedCleanups = 0;
+ const d2 = H.deferred();
+ R.register('rejects', function () {
+ return d2.promise.then(function () { return function () { rejectedCleanups++; }; });
+ });
+ const el2 = H.mkRegion(env, { id: 'rej', type: 'card-body', populate: 'rejects' });
+ R.initRegion(el2);
+ d2.reject(new Error('fetch died'));
+ await H.flush();
+ out.t6a_rejectedState = stateOf(el2);
+ R.teardownRegion(el2._juneauRegion);
+ out.t6a_rejectedRegistersNoCleanup = rejectedCleanups === 0;
+ }
+
+ // =================================================================================================================
+ // Test 6 + Test 7 - the cleanup function runs exactly once BEFORE the container is cleared, and the five teardown
+ // steps run in their fixed order. Written as an ORDER-RECORDING test, because a plausible refactor reorders it
+ // silently and every individual step would still be observably "done".
+ // =================================================================================================================
+ {
+ const { env, R, clock } = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ const order = [];
+ let pollFired = false;
+ let pollTimerNullAtAbort = null;
+ let childrenAtCleanup = null;
+ let stillSubscribedAtCleanup = null;
+ let cleanups = 0;
+ let received = [];
+
+ R.register('recorder', function (ctx, container) {
+ container.appendChild(env.el('span'));
+ ctx.on(function (msg) { received.push(msg); });
+ ctx.signal.addEventListener('abort', function () {
+ order.push('abort');
+ pollTimerNullAtAbort = el._juneauRegion.pollTimer === null;
+ });
+ return function () {
+ order.push('cleanup');
+ cleanups++;
+ childrenAtCleanup = container.childNodes.length;
+ // Step 3 (unsubscribe) has NOT run yet, so a sibling's emit still reaches this region's handler.
+ const before = received.length;
+ sibling.emit({ kind: 'during-cleanup' });
+ stillSubscribedAtCleanup = received.length === before + 1;
+ };
+ });
+ let sibling = null;
+ R.register('sibling', function (ctx) { sibling = ctx; });
+ const sibEl = H.mkRegion(env, { id: 'sib', type: 'card-body', populate: 'sibling' });
+ R.initRegion(sibEl);
+ const el = H.mkRegion(env, { id: 'rec', type: 'card-body', populate: 'recorder' });
+ R.initRegion(el);
+
+ // The declared poll timer is J0522c's to arm; the framework's own record of it is what step 0 clears, so the
+ // test arms it on the framework's handle to assert the ordering step 0 exists for.
+ el._juneauRegion.pollTimer = clock.set(function () { pollFired = true; }, 50);
+
+ R.teardownRegion(el._juneauRegion);
+ out.t7_order = order;
+ out.t7_pollTimerClearedBeforeAbort = pollTimerNullAtAbort;
+ out.t7_pollTimerCleared = el._juneauRegion === undefined;
+ clock.advance(1000);
+ out.t7_pollNeverFired = pollFired === false;
+ out.t6_cleanupsExactlyOnce = cleanups === 1;
+ out.t6_containerNotYetClearedAtCleanup = childrenAtCleanup > 0;
+ out.t7_stillSubscribedAtCleanup = stillSubscribedAtCleanup;
+ out.t7_containerClearedAfter = el.childNodes.length === 0;
+ out.t7_markRemoved = stateOf(el) === null && el._juneauRegion === undefined;
+ // Step 3 did run: after teardown the handler is unreachable from the bus even though the test's closure still
+ // holds it. This is the leak assertion.
+ const beforeCount = received.length;
+ sibling.emit({ kind: 'after-teardown' });
+ out.t7_unsubscribedAfterTeardown = received.length === beforeCount;
+ }
+
+ // =================================================================================================================
+ // Test 8 - ctx.signal aborts an in-flight fetch on collapse, and TRANSITIVELY aborts every fork() child and any
+ // live messageSignal.
+ // =================================================================================================================
+ {
+ const { env, R } = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ let ctx = null;
+ let topLevelSignal = null;
+ let forkSignal = null;
+ R.register('fetcher', function (c) {
+ ctx = c;
+ topLevelSignal = c.signal;
+ forkSignal = c.signal.fork();
+ });
+ const el = H.mkRegion(env, { id: 'f', type: 'card-body', populate: 'fetcher' });
+ R.initRegion(el);
+ out.t8_liveBeforeTeardown = topLevelSignal.aborted === false && forkSignal.aborted === false;
+ R.teardownRegion(el._juneauRegion);
+ out.t8_topLevelAborted = topLevelSignal.aborted === true;
+ out.t8_forkChildAborted = forkSignal.aborted === true;
+ out.t8_abortReasonNamesTeardown = String(topLevelSignal.reason).indexOf('torn-down') >= 0;
+ }
+
+ // =================================================================================================================
+ // Test 9a - `reason` tracks the CAUSAL EVENT, not the API that was invoked. ctx.refresh() from inside a bus
+ // handler is "message"; the identical call from a timer is "refresh".
+ // =================================================================================================================
+ {
+ const { env, R, clock } = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ const reasons = [];
+ let ctx = null;
+ let messageSignals = [];
+ R.register('reasoner', function (c) {
+ ctx = c;
+ reasons.push(c.reason);
+ messageSignals.push(c.messageSignal);
+ c.on(function () { c.refresh(); });
+ });
+ let driver = null;
+ R.register('driver', function (c) { driver = c; });
+ R.initRegion(H.mkRegion(env, { id: 'drv', type: 'card-body', populate: 'driver' }));
+ const el = H.mkRegion(env, { id: 'reasoner', type: 'card-body', populate: 'reasoner' });
+ R.initRegion(el);
+ clock.advance(0);
+ await H.flush();
+ out.t9a_initial = reasons[0];
+
+ // Synchronously inside a fan-out -> "message".
+ driver.emit({ kind: 'x' });
+ await H.flush();
+ out.t9a_fromHandler = reasons[1];
+ out.t9a_messageSignalNonNullOnMessage = messageSignals[1] !== null
+ && messageSignals[1] instanceof AbortSignal;
+
+ // The identical call from a macrotask -> "refresh".
+ clock.set(function () { ctx.refresh(); }, 5);
+ clock.advance(10);
+ await H.flush();
+ out.t9a_fromTimer = reasons[2];
+ out.t9a_messageSignalNullOnRefresh = messageSignals[2] === null;
+
+ // A second message aborts the previous delivery's messageSignal, so a region that re-populates per message
+ // gets ordering for free.
+ const priorMessageSignal = messageSignals[1];
+ driver.emit({ kind: 'y' });
+ await H.flush();
+ out.t9a_priorMessageSignalAborted = priorMessageSignal.aborted === true;
+
+ // A populate that calls refresh() unconditionally spins exactly once, not forever.
+ let spins = 0;
+ R.register('spinner', function (c) { spins++; c.refresh(); });
+ const sp = H.mkRegion(env, { id: 'spin', type: 'card-body', populate: 'spinner' });
+ R.initRegion(sp);
+ await H.flush();
+ out.t9a_selfRefreshSpinsOnce = spins;
+ }
+
+ // =================================================================================================================
+ // A region in flight is NOT re-entered: a second trigger aborts the current invocation, waits for the settle, and
+ // then runs once.
+ // =================================================================================================================
+ {
+ const { env, R } = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ const d = H.deferred();
+ const reasons = [];
+ let first = true;
+ let ctx = null;
+ let firstSignal = null;
+ R.register('coalescer', function (c) {
+ ctx = c;
+ reasons.push(c.reason);
+ if (!first) return;
+ first = false;
+ firstSignal = c.signal;
+ return d.promise;
+ });
+ const el = H.mkRegion(env, { id: 'co', type: 'card-body', populate: 'coalescer' });
+ R.initRegion(el);
+ ctx.refresh();
+ ctx.refresh();
+ ctx.refresh();
+ out.t_coalesce_callsWhileInFlight = reasons.length;
+ out.t_coalesce_inFlightSignalAborted = firstSignal.aborted === true;
+ d.resolve();
+ await H.flush();
+ out.t_coalesce_callsAfterSettle = reasons.length;
+ out.t_coalesce_reasons = reasons.slice();
+ }
+
+ // =================================================================================================================
+ // enrolIn walks a scope synchronously; a hidden region is ENROLLED but not populated until it is activated, and
+ // its first populate is "activate" and never "initial". teardownRegionsIn sweeps a subtree idempotently.
+ // =================================================================================================================
+ {
+ const { env, R, rec } = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ const reasons = {};
+ R.register('noter', function (ctx) { (reasons[ctx.id] = reasons[ctx.id] || []).push(ctx.reason); });
+ const scope = env.el('div');
+ env.body.appendChild(scope);
+ const visible = H.mkRegion(env, { id: 'vis', type: 'card-body', populate: 'noter', parent: scope });
+ const hidden = H.mkRegion(env, { id: 'hid', type: 'card-body', populate: 'noter', parent: scope, hiddenPanel: true });
+ const handles = R.enrolIn(scope);
+ out.t_enrol_count = handles.length;
+ out.t_enrol_visiblePopulated = (reasons.vis || []).slice();
+ out.t_enrol_hiddenNotPopulated = (reasons.hid || []).length === 0;
+ out.t_enrol_hiddenIsEnrolled = hidden._juneauRegion !== undefined && hidden._juneauRegion !== null;
+ out.t_enrol_hiddenState = stateOf(hidden);
+ H.activatePanel(hidden);
+ R.activateRegion(hidden._juneauRegion);
+ out.t_enrol_hiddenReasonOnActivate = (reasons.hid || []).slice();
+ // A second activation produces no second populate.
+ R.activateRegion(hidden._juneauRegion);
+ out.t_enrol_activateOnce = (reasons.hid || []).length === 1;
+
+ R.teardownRegionsIn(scope);
+ out.t_teardown_swept = visible._juneauRegion === undefined && hidden._juneauRegion === undefined;
+ R.teardownRegionsIn(scope);
+ out.t_teardown_idempotent = rec.errors.length === 0;
+
+ // A marked node found DISCONNECTED is a loud named error rather than a silent no-op.
+ const orphan = H.mkRegion(env, { id: 'orphan', type: 'card-body', populate: 'noter' });
+ R.initRegion(orphan);
+ orphan.remove();
+ R.initRegion(orphan);
+ out.t_orphan_errorLogged = rec.errorsMatching('removed without a').length === 1;
+ }
+
+ // =================================================================================================================
+ // Test 50b (runtime half) - ctx.write from a NON-TABLE region carries the CSRF header, refuses a cross-origin URL,
+ // refuses a safe method, issues zero network traffic on a refusal, aborts a prior in-flight write from the same
+ // region, and does not touch the region's loading/ok/error state in any of those cases.
+ // =================================================================================================================
+ {
+ const { env, R } = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ let ctx = null;
+ R.register('writer', function (c) { ctx = c; });
+ const host = env.el('div'); // a card grid, not a table: there is no table[data-juneau-csrf] to read from.
+ host.setAttribute('data-juneau-csrf', 'tok-123');
+ env.body.appendChild(host);
+ const el = H.mkRegion(env, { id: 'w', type: 'card-body', populate: 'writer', parent: host });
+ R.initRegion(el);
+
+ const calls = [];
+ const pending = [];
+ env.setFetch(function (url, init) {
+ calls.push({ url: url, init: init });
+ const d = H.deferred();
+ pending.push(d);
+ return d.promise;
+ });
+
+ const stateBefore = stateOf(el);
+ const p1 = ctx.write('/rest/x/act', { a: 1 }, { idempotencyKey: 'k-1' });
+ out.t50b_headerStamped = calls[0].init.headers['X-Csrf-Token'] === 'tok-123';
+ out.t50b_contentType = calls[0].init.headers['Content-Type'] === 'application/json';
+ out.t50b_method = calls[0].init.method;
+ out.t50b_idempotencyMerged = JSON.parse(calls[0].init.body).idempotencyKey === 'k-1';
+ out.t50b_notBareCtxSignal = calls[0].init.signal !== ctx.signal;
+ const firstSignal = calls[0].init.signal;
+
+ // A second write from the same region aborts the first rather than racing it.
+ const p2 = ctx.write('/rest/x/act', { a: 2 });
+ out.t50b_priorWriteAborted = firstSignal.aborted === true;
+ out.t50b_secondWriteLive = calls[1].init.signal.aborted === false;
+ pending[0].resolve(H.jsonResponse({ ok: 1 }));
+ pending[1].resolve(H.jsonResponse({ ok: 1 }, { status: 500 }));
+ const r2 = await p2;
+ out.t50b_nonTwoHundredResolvesNotRejects = r2.ok === false && r2.status === 500;
+ await p1.catch(function () {});
+
+ // Three refusals, each with a marker, a visible message and ZERO network traffic.
+ const callsBefore = calls.length;
+ const refusals = {};
+ for (const [name, args] of Object.entries({
+ safeMethod: ['/rest/x/act', {}, { method: 'GET' }],
+ crossOrigin: ['https://evil.example/act', {}, {}],
+ dotDot: ['/rest/../etc', {}, {}]
+ })) {
+ try { await ctx.write(...args); refusals[name] = 'RESOLVED'; }
+ catch (e) { refusals[name] = e.reason; }
+ }
+ out.t50b_refusalReasons = refusals;
+ out.t50b_refusalsIssuedNoTraffic = calls.length === callsBefore;
+ out.t50b_refusalVisible = el.querySelectorAll('[data-juneau-region-write-error]').length === 3;
+ out.t50b_stateUntouched = stateOf(el) === stateBefore && stateOf(el) === 'ok';
+
+ // No token on the host -> missing-token refusal, never a silent unauthenticated send.
+ const el2 = H.mkRegion(env, { id: 'w2', type: 'card-body', populate: 'writer' });
+ R.initRegion(el2);
+ const ctx2 = ctx;
+ let missing = null;
+ try { await ctx2.write('/rest/x/act', {}); } catch (e) { missing = e.reason; }
+ out.t50b_missingTokenReason = missing;
+ }
+
+ // =================================================================================================================
+ // Test 8b - THE AbortController BASELINE FAILS LOUD. A page carrying three regions in an environment with no
+ // AbortController populates NOTHING, shows the error state on each, logs EXACTLY ONE console error naming the
+ // missing platform feature, and hands no region a ctx at all - so there is no ctx whose signal is undefined or a
+ // polyfilled look-alike.
+ // =================================================================================================================
+ {
+ const { env, R, rec } = H.load(rendersJsPath, viewsJsPath, regionsJsPath, { noAbortController: true });
+ let populates = 0;
+ const seenSignals = [];
+ R.register('never', function (ctx) { populates++; seenSignals.push(ctx && ctx.signal); });
+ const els = ['a', 'b', 'c'].map(function (id) {
+ return H.mkRegion(env, { id: id, type: 'card-body', populate: 'never' });
+ });
+ const handles = els.map(function (el) { return R.initRegion(el); });
+ out.t8b_populates = populates;
+ out.t8b_noHandles = handles.every(function (h) { return h === null; });
+ out.t8b_allErrored = els.map(stateOf);
+ out.t8b_exactlyOneError = rec.errorsMatching('requires AbortController').length === 1;
+ out.t8b_totalErrors = rec.errors.length;
+ out.t8b_noCtxHandedOut = seenSignals.length === 0;
+ out.t8b_messageVisible = els.every(function (el) {
+ return (el.textContent || '').indexOf('AbortController') >= 0;
+ });
+ }
+
+ // =================================================================================================================
+ // The exported surface: register / resolve / initRegion / ready are published on JuneauViews.regions, and the
+ // contract version is the region envelope's own and not another envelope's.
+ // =================================================================================================================
+ {
+ const { NS, R } = H.load(rendersJsPath, viewsJsPath, regionsJsPath);
+ out.tx_exports = ['register', 'resolve', 'initRegion', 'ready'].map(function (n) { return typeof R[n]; });
+ out.tx_contractVersion = R.CONTRACT_VERSION;
+ out.tx_contractVersionIndependent = R.CONTRACT_VERSION !== NS.CONTRACT_VERSION
+ || NS.CONTRACT_VERSION === undefined;
+ out.tx_regionTypes = R.REGION_TYPES.slice();
+ out.tx_reserved = R.RESERVED_POPULATOR_NAMES.slice();
+ out.tx_caps = { queue: R.BUS_QUEUE_DEPTH_CAP, replay: R.REPLAY_BUFFER_CAP, deadline: R.BARRIER_DEADLINE_MS };
+ out.tx_publishedOnSharedNamespace = NS.regions === R && typeof NS.init === 'object';
+ }
+
+ process.stdout.write(JSON.stringify(out));
+})().catch(function (e) {
+ process.stderr.write(String(e && e.stack ? e.stack : e));
+ process.exit(1);
+});