mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-08-07 01:13:01 +02:00
8ed15f1142
* web: initial-load module-graph waterfall
* web: prefetch initial API data on cold load (faster first paint)
Second phase of the initial-load fix (after the module-graph preload in
8915f8c). Even with the graph preloaded, the first /api/* fetch waited for the
~806KB bundle to download, parse, and boot. An early <script type="module"
async> boot module (main/boot-prefetch.js) now fires the first-load (since=0)
requests in parallel with the module graph (at priority:'high') and stashes the
in-flight Response promises on window.__PM_BOOT__; refresh() consumes them on its
first cold refresh via a new responsePromise option on the data-fetchers instead
of issuing its own requests.
Cold loads only: a synchronous localStorage marker (pm:cache-present, maintained
by the cache write-back / clear / disable paths) suppresses the prefetch on warm
revisits, leaving the FC2 seed-then-delta path untouched. Message endpoints are
skipped in private mode (data-pm-chat="false"), mirroring the /api/messages 404
(Invariant II / PS6). Pure pre-warm: an absent or rejected prefetch re-fetches,
so it is never load-bearing (FC7).
Read-side only; no API/DB/ingestor change, no new dependency. Adds ACCEPTANCE
EF-A1/EF-A2/EF-R1; 100% line/branch/func coverage on the new module (lcov);
rspec + npm test green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
135 lines
4.4 KiB
Ruby
135 lines
4.4 KiB
Ruby
# Copyright © 2025-26 l5yth & contributors
|
|
#
|
|
# Licensed 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.
|
|
|
|
# frozen_string_literal: true
|
|
|
|
require "spec_helper"
|
|
require "tmpdir"
|
|
require "fileutils"
|
|
require "json"
|
|
|
|
# Unit coverage for the import-map builder (SPEC AV3). The builder is exercised
|
|
# against a temporary asset tree so the assertions are independent of the real
|
|
# bundle and of the live version string.
|
|
RSpec.describe PotatoMesh::App::AssetImportMap do
|
|
around do |example|
|
|
Dir.mktmpdir("potato-mesh-importmap-") do |dir|
|
|
@js_root = dir
|
|
FileUtils.mkdir_p(File.join(dir, "app", "__tests__"))
|
|
File.write(File.join(dir, "theme.js"), "// theme")
|
|
File.write(File.join(dir, "app", "main.js"), "// main")
|
|
File.write(File.join(dir, "app", "config.js"), "// config")
|
|
File.write(File.join(dir, "app", "__tests__", "main.test.js"), "// test")
|
|
example.run
|
|
end
|
|
end
|
|
|
|
describe ".module_paths" do
|
|
it "lists served modules as /assets/js paths, sorted" do
|
|
expect(described_class.module_paths(@js_root)).to eq(
|
|
[
|
|
"/assets/js/app/config.js",
|
|
"/assets/js/app/main.js",
|
|
"/assets/js/theme.js",
|
|
],
|
|
)
|
|
end
|
|
|
|
it "excludes __tests__ files" do
|
|
expect(described_class.module_paths(@js_root)).not_to include(
|
|
a_string_including("__tests__"),
|
|
)
|
|
end
|
|
|
|
it "returns an empty list when the directory is absent" do
|
|
expect(described_class.module_paths(File.join(@js_root, "nope"))).to eq([])
|
|
end
|
|
end
|
|
|
|
describe ".preload_paths" do
|
|
it "lists only the app ES modules (excludes the classic top-level scripts)" do
|
|
# theme.js is a classic <script>, not an ES module, so it must never be
|
|
# emitted as <link rel="modulepreload"> (that would fetch it as a module
|
|
# and double-load it). Only /assets/js/app/** modules are preloaded.
|
|
expect(described_class.preload_paths(@js_root)).to eq(
|
|
[
|
|
"/assets/js/app/config.js",
|
|
"/assets/js/app/main.js",
|
|
],
|
|
)
|
|
end
|
|
|
|
it "excludes __tests__ files" do
|
|
expect(described_class.preload_paths(@js_root)).not_to include(
|
|
a_string_including("__tests__"),
|
|
)
|
|
end
|
|
|
|
it "returns an empty list when the directory is absent" do
|
|
expect(described_class.preload_paths(File.join(@js_root, "nope"))).to eq([])
|
|
end
|
|
end
|
|
|
|
describe ".preload_html" do
|
|
it "emits one version-stamped modulepreload link per app module" do
|
|
html = described_class.preload_html(@js_root, "1.2.3")
|
|
|
|
expect(html).to eq(
|
|
%(<link rel="modulepreload" href="/assets/js/app/config.js?v=1.2.3">\n) +
|
|
%(<link rel="modulepreload" href="/assets/js/app/main.js?v=1.2.3">),
|
|
)
|
|
end
|
|
|
|
it "never preloads the classic top-level scripts" do
|
|
expect(described_class.preload_html(@js_root, "1.2.3")).not_to include("theme.js")
|
|
end
|
|
|
|
it "is stable across repeated calls (memoized per root/version)" do
|
|
first = described_class.preload_html(@js_root, "7.0.0")
|
|
second = described_class.preload_html(@js_root, "7.0.0")
|
|
|
|
expect(second).to equal(first)
|
|
end
|
|
end
|
|
|
|
describe ".document" do
|
|
it "maps each module path to its version-stamped URL" do
|
|
doc = described_class.document(@js_root, "1.2.3")
|
|
|
|
expect(doc).to eq(
|
|
"imports" => {
|
|
"/assets/js/app/config.js" => "/assets/js/app/config.js?v=1.2.3",
|
|
"/assets/js/app/main.js" => "/assets/js/app/main.js?v=1.2.3",
|
|
"/assets/js/theme.js" => "/assets/js/theme.js?v=1.2.3",
|
|
},
|
|
)
|
|
end
|
|
end
|
|
|
|
describe ".json" do
|
|
it "emits valid JSON equal to the document" do
|
|
parsed = JSON.parse(described_class.json(@js_root, "9.9.9"))
|
|
|
|
expect(parsed).to eq(described_class.document(@js_root, "9.9.9"))
|
|
end
|
|
|
|
it "is stable across repeated calls (memoized per root/version)" do
|
|
first = described_class.json(@js_root, "7.0.0")
|
|
second = described_class.json(@js_root, "7.0.0")
|
|
|
|
expect(second).to equal(first)
|
|
end
|
|
end
|
|
end
|