refactor(sync): split sync_state into sync_chain and sync_note_transport for granularity#2091
Conversation
sync_state previously returned Err when the Note Transport Layer fetch failed, taking down the on-chain portion of the sync with it. The NTL fetch is now treated as best-effort: its failure is caught, logged, and surfaced via a new SyncSummary.ntl_error: Option<String> field (ntlError() on the web client). Applications can check the field to disable private-note features while keeping public-note flows working.
Previously the whole ClientError was stringified, which produced the
terse top-level Display ("note transport error") because ClientError's
variants delegate detail to the error source chain rather than their
own Display impl. Pattern-match ClientError::NoteTransportError and
stringify the inner error directly, which carries the informative
Display (e.g. "note transport network error: Fetch notes failed: ...").
Also stop silently swallowing non-NTL errors from fetch_transport_notes
(store failures from cursor/import/sync-height lookups). Those are real
issues unrelated to NTL infra and now propagate as before.
| /// Error message from the Note Transport Layer if its fetch failed during the sync. | ||
| /// `None` means NTL was either disabled or succeeded. | ||
| pub ntl_error: Option<String>, |
There was a problem hiding this comment.
Why not pass the error directly here, as opposed to a full string?
There was a problem hiding this comment.
The problem is we'd have to implement PartialEq and Clone for NoteTransportError, and it has inside a Connection variant that has Box<dyn Error + Send + Sync>, which can implement neither and isn't serializable either.
Having the typed error there would be nice on the rust side but the WebClient will end up consuming a string anyway, right? I think it'd only be worth to do the necessary to have the error type only if we want to pattern match with it, until then a String would be simpler for now.
Let me know if we really don't want it to be a String though and we can take the necessary measures.
SantiagoPittella
left a comment
There was a problem hiding this comment.
It is conceptually odd continuing despite the error here. In case that the NTL fails, how can we continue fetch that later? Should this behavior be removed entirely into its own sync function?
There was a problem hiding this comment.
Lets update this function;s documentation including this behavior
I agree a bit. This is why I suggested potentially having separate APIs for this (instead of putting everything behind |
decouple NTL fetching from Client::sync_state. sync_state now only performs the on-chain sync; sync_note_transport fetches private notes and sync_all runs both sequentially, failing fast on either. drop the ntl_error field from SyncSummary and expose syncNoteTransport / syncAll on the web client, with the sync lock made method-aware so same-method coalescing is preserved while different methods serialize.
My bad, I just thought that this was going to be good enough for the user but after thinking about it I see that it's not the best UX because the user doesn't have the possibility to sync the NTL without syncing the rest if he wants to. I have pushed changes regarding this, the WebClient part is kinda dirty for now so I'll see to make it cleaner and perhaps if I (or you) think that the approach should be different I'll change it. |
replace the two-phase acquire/release lock with a single withSyncLock(dbId, methodId, fn, timeoutMs) callback. native Promise sharing handles coalescing and error propagation, removing the manual waiter queues, syncGeneration tracking, and dual timeout layers. same semantics: same-method coalesces, different-method serializes via the Web Lock (or the WASM mutex as fallback), cross-tab coordination preserved.
the tests were calling sync_state expecting it to also fetch NTL notes, which no longer holds after the sync_state/sync_note_transport split. replace sync_state with sync_note_transport in the four pure-NTL tests. in fetch_private_notes_finds_note_committed_at_sync_height keep the first sync_state (setup: advances sync_height while NTL is empty, which is what creates the race) and swap only the second call to sync_note_transport (the behavior under test: NTL lookback).
…ault Client::sync_state now runs NTL then chain (failing fast on either) — the original combined semantics preserved at the original name. The on-chain-only path is exposed as Client::sync_chain. Client::sync_all is dropped since it was redundant with the new sync_state. JS parity: MidenClient.sync() stays combined, syncChain() is new, syncAll() is removed.
The earlier edits to sync_note_transport were needed while sync_state was chain-only. With sync_state back to its combined semantics, the original calls work unchanged, matching next.
sync_state into sync_chain and sync_note_transport for granularity
The timeout only rejected the caller's promise; the underlying sync kept running in the background and eventually wrote to the store anyway. Drop the misleading API rather than keep a contract we cannot deliver. Callers who want a bail-out can wrap with Promise.race themselves.
The previous commit removed the `timeout` option from MidenClient.sync/syncChain/syncNoteTransport at runtime but missed the public-facing .d.ts. No CI job compares api-types.d.ts parameter shapes against the js implementation (check:standalone-types only verifies return types of forwarders in standalone.js), so the drift slipped through.
|
It wasn't my goal to make those WebClient changes in this PR but had to because I realized that otherwise it'd ended in very complex logic given that we now have 3 methods for syncing instead of just one. I think that making another PR for those changes as a follow-up would've been more churn for everyone. |
juan518munoz
left a comment
There was a problem hiding this comment.
I'd probably rename SyncSummary to something like SyncChainSummary and create a wrapper type like:
SyncSummary = SyncChainSummary
This is just a nit that can be ommitted, but in my opinion helps with code clarity.
| * @returns {Promise<SyncSummary>} The sync summary. | ||
| */ | ||
| async sync(opts) { | ||
| async sync() { |
There was a problem hiding this comment.
why have we removed the opts (timeout) here?
ropped the web
timeoutoption (andsyncStateWithTimeout) since it never cancelled the underlying sync.
Shouldn't we instead focus on making the timeout work?
There was a problem hiding this comment.
I evaluated doing that in this PR but it implicating making a lot of changes. I will give it another try though but we gotta find out if it's actually necessary and how to make it work properly (and if it's even worth it to do so because of the complexity cost). I'll probably open an issue in the new web-sdk repository after evaluating this. I will have to migrate half of this PR over there anyway.
There was a problem hiding this comment.
It can be left for a later revision, if so let's be sure to track it.
|
Heads-up: the web-sdk-related changes from this PR have been migrated to a new PR on the web-sdk repo: 0xMiden/web-sdk#30. This is part of the ongoing split of web/WASM components from miden-client into a dedicated repo (#1992 / #2135). Once that split lands, the Please continue this PR with the miden-client-only changes; the web-sdk-side changes are tracked in 0xMiden/web-sdk#30. If you have write access to your branch, the cleanest follow-up is to drop the web-sdk file changes from this PR. Otherwise they'll naturally fall out when you rebase after #1992 / #2135 merges. Note: the migrated web-sdk PR contains 3-way merge conflicts (drift between miden-client |
…couple-1930 # Conflicts: # crates/web-client/js/client.js # crates/web-client/js/constants.js # crates/web-client/js/index.js # crates/web-client/js/node/napi-compat.js # crates/web-client/js/resources/transactions.js # crates/web-client/js/syncLock.js # crates/web-client/js/types/api-types.d.ts # crates/web-client/js/types/index.d.ts # crates/web-client/js/workers/web-client-methods-worker.js # crates/web-client/scripts/check-method-classification.js # crates/web-client/src/sync.rs # crates/web-client/test/node-adapter.ts # crates/web-client/test/sync_lock.test.ts # crates/web-client/test/test-helpers.ts # docs/typedoc/web-client/classes/MidenClient.md
SantiagoPittella
left a comment
There was a problem hiding this comment.
Looks good, maybe we can improve a bit the documentation of
pub async fn sync_state(&mut self) -> Result<SyncSummary, ClientError> {
With a somewhat brief explanation of what it does
TomasArrachea
left a comment
There was a problem hiding this comment.
LGTM.
Agreed with @SantiagoPittella that we could expand the Client::sync_state docs to explain briefly what is the sync state and mention the two internal methods in case the user needs them. BTW the now sync_chain method docs need updating.
…' into jere/syncstate-ntl-decouple-1930
|
You're right, I expanded the docs for
I think it's up to date, it has the docs that |
…mary Rewrite the step lists in StateSync::sync_state and Client::sync_chain so they match the actual order of operations and reference OnNoteReceived / NoteScreener. Make Client::sync_chain delegate to StateSync for the detailed pipeline to avoid duplicating the bullet list across both layers. Add a new_private_notes field to SyncSummary, populated from the IDs that sync_note_transport now returns (sourced from import_notes). This restores visibility into NTL-imported notes that would otherwise be missed by committed_notes when the lookback window catches a note already committed before the current sync height. Cover the field in the existing transport test.
|
|
||
| println!("State synced to block {}", new_details.block_num); | ||
| println!("New public notes: {}", new_details.new_public_notes.len()); | ||
| println!("New private notes: {}", new_details.new_private_notes.len()); |
There was a problem hiding this comment.
This is a bit ambiguous: you can get private notes from the chain itself (or rather, we get the inclusion information for a private note that the client knew about), but I believe this just really displays the private notes retrieved from the NTL, right? If so, we'd need to make this clearer IMO.
| let new_private_notes = self.sync_note_transport().await?; | ||
| let mut summary = self.sync_chain().await?; | ||
| summary.new_private_notes = new_private_notes; | ||
| Ok(summary) |
There was a problem hiding this comment.
nit: We could add something like SyncSummary::from_ntl_sync(fetched_note_ids: Vec<NoteId>) and have SyncSummary::from_chain_sync(/* all other parameters */) and then combine them with SyncSummary::combine_with() here to avoid mutating the internal field, and also avoid having to initialize the SyncSummary with an empty list of private notes every time. I wonder if we could also make all fields private instead of pub by default.
Splits
sync_stateinto two functions that can be called independently. Users that want to sync to the tip of the chain but don't care about the NTL can now callsync_chain, or if user wants to sync the NTL directly they could callsync_note_transport.Closes (Partially) 0xMiden/web-sdk#92