feat(sync): add typed presence projection
req: sync/001 req: sync/005
This commit is contained in:
@@ -2201,8 +2201,8 @@ async fn adversarial_wire_inputs_are_rejected_before_partial_application() -> We
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn typed_broadcast_applies_generated_batch_over_sse() -> WebDriverResult<()> {
|
||||
// test req: sync/004
|
||||
async fn typed_presence_join_leave_updates_generated_atom_over_sse() -> WebDriverResult<()> {
|
||||
// test req: sync/001 req: sync/004 req: sync/005
|
||||
let app_port = available_port();
|
||||
let app_addr = format!("127.0.0.1:{app_port}");
|
||||
let mut app_command = Command::new(env!("CARGO_BIN_EXE_hemx-kanban-example"));
|
||||
@@ -2222,25 +2222,57 @@ async fn typed_broadcast_applies_generated_batch_over_sse() -> WebDriverResult<(
|
||||
|
||||
let result = async {
|
||||
driver.goto(&format!("http://{app_addr}/")).await?;
|
||||
wait_until(
|
||||
&driver,
|
||||
"return document.body.textContent.includes('tick #7')",
|
||||
)
|
||||
.await?;
|
||||
let resources = driver
|
||||
.execute(
|
||||
"return performance.getEntriesByType('resource').map(entry => entry.name).filter(name => name.includes('/sync/broadcast'))",
|
||||
wait_until(&driver, "return document.body.textContent.includes('tick #0')").await?;
|
||||
let proof = driver
|
||||
.execute_async(
|
||||
r#"
|
||||
const done = arguments[arguments.length - 1];
|
||||
(async () => {
|
||||
const root = document.querySelector('[data-hemx-root]');
|
||||
const apply = (url) => new Promise((resolve, reject) => {
|
||||
const source = new EventSource(url);
|
||||
const timeout = setTimeout(() => {
|
||||
source.close();
|
||||
reject(new Error(`presence event timed out: ${url}`));
|
||||
}, 5000);
|
||||
source.addEventListener('hemx', (event) => {
|
||||
clearTimeout(timeout);
|
||||
const normalized = event.data.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const padded = normalized + '='.repeat((4 - normalized.length % 4) % 4);
|
||||
const raw = atob(padded);
|
||||
const bytes = Uint8Array.from(raw, (character) => character.charCodeAt(0));
|
||||
window.hemx.applyBatch(bytes.buffer, root);
|
||||
source.close();
|
||||
resolve(document.body.textContent);
|
||||
});
|
||||
source.onerror = () => {
|
||||
clearTimeout(timeout);
|
||||
source.close();
|
||||
reject(new Error(`presence event failed: ${url}`));
|
||||
};
|
||||
});
|
||||
const joinedAda = await apply('/sync/broadcast?channel=board&action=join&member=ada');
|
||||
const duplicateAda = await apply('/sync/broadcast?channel=board&action=join&member=ada');
|
||||
const joinedGrace = await apply('/sync/broadcast?channel=board&action=join&member=grace');
|
||||
const leftAda = await apply('/sync/broadcast?channel=board&action=leave&member=ada');
|
||||
done({
|
||||
joinedAda: joinedAda.includes('tick #1'),
|
||||
duplicateAda: duplicateAda.includes('tick #1'),
|
||||
joinedGrace: joinedGrace.includes('tick #2'),
|
||||
leftAda: leftAda.includes('tick #1'),
|
||||
});
|
||||
})().catch((error) => done({ error: String(error), stack: error?.stack }));
|
||||
"#,
|
||||
Vec::new(),
|
||||
)
|
||||
.await?
|
||||
.json()
|
||||
.clone();
|
||||
assert!(
|
||||
resources
|
||||
.as_array()
|
||||
.is_some_and(|resources| !resources.is_empty()),
|
||||
"typed broadcast SSE request was not observed: {resources}"
|
||||
);
|
||||
assert!(proof["error"].is_null(), "typed presence failed: {proof}");
|
||||
assert_eq!(proof["joinedAda"], true, "{proof}");
|
||||
assert_eq!(proof["duplicateAda"], true, "{proof}");
|
||||
assert_eq!(proof["joinedGrace"], true, "{proof}");
|
||||
assert_eq!(proof["leftAda"], true, "{proof}");
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user