feat(sync): add typed flat patch boundary
req: sync/002 req: sync/003
This commit is contained in:
@@ -11,5 +11,6 @@ hemx-core = { path = "../hemx-core" }
|
||||
wasm-bindgen = "=0.2.125"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1"
|
||||
thirtyfour = "0.36"
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
|
||||
|
||||
+126
-2
@@ -191,6 +191,98 @@ async fn client_handler_applies_effect_batch_without_network() -> WebDriverResul
|
||||
result.and(quit)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn flat_patch_persists_offline_then_uploads_with_same_operation_identity(
|
||||
) -> WebDriverResult<()> {
|
||||
// test req: sync/002 req: sync/003
|
||||
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.expect("workspace root")
|
||||
.to_owned();
|
||||
let (package, bootstrap, rendered) = build_kanban_artifact(&workspace);
|
||||
let runtime = workspace.join("hemx-js/runtime/hemx.js");
|
||||
let server = StaticServer::start(
|
||||
package,
|
||||
runtime,
|
||||
bootstrap,
|
||||
rendered,
|
||||
"kanban_client",
|
||||
Some(kanban_app_assets(&workspace)),
|
||||
);
|
||||
|
||||
let webdriver_port = available_port();
|
||||
let webdriver_addr = format!("127.0.0.1:{webdriver_port}");
|
||||
let mut webdriver = Command::new("geckodriver");
|
||||
webdriver.arg("--port").arg(webdriver_port.to_string());
|
||||
let _webdriver = ProcessGuard::start(webdriver, &webdriver_addr);
|
||||
let mut caps = DesiredCapabilities::firefox();
|
||||
caps.set_headless()?;
|
||||
let driver = WebDriver::new(&format!("http://{webdriver_addr}"), caps).await?;
|
||||
|
||||
let result = async {
|
||||
driver.goto(&server.url()).await?;
|
||||
wait_until(
|
||||
&driver,
|
||||
"const root = document.querySelector('[data-hemx-root]'); return root.hasAttribute('data-hemx-client-ready') && root.hasAttribute('data-hemx-sync-ready')",
|
||||
)
|
||||
.await?;
|
||||
driver
|
||||
.execute(
|
||||
"window.__clientErrors = []; document.querySelector('[data-hemx-root]').addEventListener('hemx:client-error', event => window.__clientErrors.push(event.detail)); Object.defineProperty(Navigator.prototype, 'onLine', { configurable: true, get: () => false }); document.querySelector('[data-hemx-client-event=drop]').dispatchEvent(new Event('drop', { bubbles: true })); return true",
|
||||
Vec::new(),
|
||||
)
|
||||
.await?;
|
||||
wait_until(
|
||||
&driver,
|
||||
"return document.querySelector('[data-hemx-root]').getAttribute('data-hemx-sync-pending') === '1'",
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
driver
|
||||
.find(By::Css("#kanban-status"))
|
||||
.await?
|
||||
.text()
|
||||
.await?
|
||||
.contains("Moved 1 with drop"),
|
||||
"ordinary EffectBatch did not apply alongside the sync patch"
|
||||
);
|
||||
assert_eq!(
|
||||
driver
|
||||
.execute(
|
||||
"return performance.getEntriesByType('resource').filter(entry => entry.name.endsWith('/sync/patches')).length",
|
||||
Vec::new(),
|
||||
)
|
||||
.await?
|
||||
.json(),
|
||||
&serde_json::json!(0),
|
||||
"offline patch attempted a network request"
|
||||
);
|
||||
|
||||
driver
|
||||
.execute(
|
||||
"Object.defineProperty(Navigator.prototype, 'onLine', { configurable: true, get: () => true }); window.dispatchEvent(new Event('online')); return true",
|
||||
Vec::new(),
|
||||
)
|
||||
.await?;
|
||||
wait_until(
|
||||
&driver,
|
||||
"const root = document.querySelector('[data-hemx-root]'); return root.getAttribute('data-hemx-sync-pending') === '0' && root.hasAttribute('data-hemx-sync-ack')",
|
||||
)
|
||||
.await?;
|
||||
let identity = driver
|
||||
.execute(
|
||||
"const root = document.querySelector('[data-hemx-root]'); return { ack: root.getAttribute('data-hemx-sync-ack'), uuid: /^[0-9a-f-]{36}$/.test(root.getAttribute('data-hemx-sync-ack')) }",
|
||||
Vec::new(),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(identity.json()["uuid"], true, "{identity:?}");
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
let _ = driver.quit().await;
|
||||
result
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn kanban_public_api_offline_sync_journey_converges_without_duplicate_replay(
|
||||
) -> WebDriverResult<()> {
|
||||
@@ -1457,12 +1549,14 @@ fn newest_generated_bootstrap(build_dir: &Path, prefix: &str) -> PathBuf {
|
||||
struct AppAssets {
|
||||
module: PathBuf,
|
||||
service_worker: PathBuf,
|
||||
sync_runtime: PathBuf,
|
||||
}
|
||||
|
||||
fn kanban_app_assets(workspace: &Path) -> AppAssets {
|
||||
AppAssets {
|
||||
module: workspace.join("examples/kanban/static/command-log.js"),
|
||||
service_worker: workspace.join("examples/kanban/static/offline.js"),
|
||||
sync_runtime: workspace.join("hemx-sync/runtime/hemx-sync.js"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1577,6 +1671,32 @@ fn serve(
|
||||
fs::read(&app_assets.expect("checked app assets").service_worker)
|
||||
.expect("read service worker"),
|
||||
),
|
||||
"/hemx-sync.js" if app_assets.is_some() => (
|
||||
"text/javascript; charset=utf-8",
|
||||
fs::read(&app_assets.expect("checked app assets").sync_runtime)
|
||||
.expect("read sync runtime"),
|
||||
),
|
||||
"/sync/patches" if app_assets.is_some() => {
|
||||
let body = first.split("\r\n\r\n").nth(1).unwrap_or("");
|
||||
let patch: serde_json::Value = serde_json::from_str(body).expect("valid flat patch");
|
||||
let idempotency_key = patch["idempotencyKey"]
|
||||
.as_str()
|
||||
.expect("flat patch idempotency key");
|
||||
let operation_id = patch["operationId"]
|
||||
.as_str()
|
||||
.expect("flat patch operation id");
|
||||
assert_eq!(
|
||||
operation_id, idempotency_key,
|
||||
"interaction operation and idempotency identities diverged"
|
||||
);
|
||||
(
|
||||
"application/json; charset=utf-8",
|
||||
format!(
|
||||
r#"{{"idempotencyKey":"{idempotency_key}","operationId":"{operation_id}"}}"#
|
||||
)
|
||||
.into_bytes(),
|
||||
)
|
||||
}
|
||||
"/sync/context" if app_assets.is_some() => (
|
||||
"application/json; charset=utf-8",
|
||||
br#"{"accountPartition":"demo:demo"}"#.to_vec(),
|
||||
@@ -1586,7 +1706,11 @@ fn serve(
|
||||
let status = if path == "/"
|
||||
|| path == "/hemx.js"
|
||||
|| path == "/hemx.client.js"
|
||||
|| ((path == "/app.js" || path == "/offline.js" || path == "/sync/context")
|
||||
|| ((path == "/app.js"
|
||||
|| path == "/offline.js"
|
||||
|| path == "/hemx-sync.js"
|
||||
|| path == "/sync/context"
|
||||
|| path == "/sync/patches")
|
||||
&& app_assets.is_some())
|
||||
|| path.starts_with(&format!("/{asset_stem}"))
|
||||
{
|
||||
@@ -1602,7 +1726,7 @@ fn serve(
|
||||
|
||||
fn fixture_html(rendered: &str, has_app_module: bool) -> String {
|
||||
let app_module = if has_app_module {
|
||||
"<script type=\"module\" src=\"/app.js\"></script>"
|
||||
"<script type=\"module\" src=\"/app.js\"></script><script type=\"module\" src=\"/hemx-sync.js\"></script>"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user