Skip to content

[User storage] New browser API#277600

Open
tsullivan wants to merge 10 commits into
elastic:mainfrom
tsullivan:user-storage/new-browser-api
Open

[User storage] New browser API#277600
tsullivan wants to merge 10 commits into
elastic:mainfrom
tsullivan:user-storage/new-browser-api

Conversation

@tsullivan

@tsullivan tsullivan commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

Closes #276110

Extends the browser User Storage client so consumers no longer reimplement availability checks or lazy-cache readiness themselves, then migrates the two in-repo consumers onto the new APIs.

New client APIs:

  • isAvailable() synchronous signal to flag if consumers can read/write to storage
  • get() changed to an async, resolved read
  • getState$() load-state observable
  • update() read-modify-write helper

API additions

Change Description Impact
isAvailable() Whether User Storage is usable for the current user, seeded from a new server-injected available flag (false for anonymous / no profile_uid / authz failure at inject time). A single condition gates both read-preload and writes. Consumers gate save/delete UI on the client directly instead of a separate user-profile request.
get() → async resolved Returns a Promise that resolves once the value is known (awaiting a lazy fetch) and rejects on fetch failure. peek() remains the synchronous cache-only read. Breaking (browser JS API): get() callers must await; read-modify-write no longer builds on unhydrated cache state.
getState$() load-state observable Emits { status: 'loading' | 'resolved' | 'error', value }. UI can show a fallback and keep destructive actions disabled until resolved.
update() read-modify-write helper Awaits the resolved current value, applies an updater, persists the result; no-ops when the updater returns the same reference. Structured mutations of lazy keys are safe by default (last-write-wins, no concurrency retry).

Server plumbing

The available flag is computed in the rendering service (from profile_uid presence, defaulting to false on missing service / anonymous / authz failure) and threaded to the browser through the shared injected-metadata types.

Consumer migrations

Consumer Change Impact
Date-range presets service savePreset/deletePreset use update(); getCanWrite$() wraps isAvailable() as a one-shot observable; userProfile dependency removed. Fixes a data-loss bug where saving before hydration overwrote existing presets; drops the profile-request workaround.
Navigation customization service Two get() calls switched to peek(). No behavior change — the key is preload: true, so peek() returns the same cached value.
useUserStorage hook Returns [value, setter, { status }] (backed by getState$) instead of [value, setter]. UI consumers get load-state; no existing callers affected.

Identify risks

  • get() is now async (breaking browser JS API). A caller treating it as synchronous would get a Promise. Mitigation: audited and migrated every in-repo consumer; full type-check passes, so no caller compiles against the old signature.
  • update() is last-write-wins. Concurrent writers (e.g. multiple tabs) can still clobber each other. Mitigation: documented in the API; true optimistic concurrency is out of scope (no version/etag in the stack today) and noted as a follow-up.

@tsullivan tsullivan changed the title User storage/new browser api [User storage] New browser API Jul 11, 2026
@tsullivan tsullivan self-assigned this Jul 11, 2026
- `isAvailable()`/`isAvailable$()` and `canWrite()`/`canWrite$()` — seeded from a new server-injected `available` boolean (computed from `profile_uid` presence in the rendering service), so consumers stop making a separate user-profile network request.
- `get()` reshaped to **async** — resolves once the value is truly known (awaits lazy hydration), rejects on fetch failure. `peek()` is unchanged and remains the sanctioned synchronous, cache-only read.
- `getState$()` — new load-state observable (`loading`/`resolved`/`error`).
- `update(key, default, updater)` — safe resolved read-modify-write; no-ops (skips the write) when the updater returns the same reference.

Consumers migrated:
- `date_range_picker_presets_service.ts` — `savePreset`/`deletePreset` now use `update()` (fixing the actual data-loss race), `getCanWrite$` delegates to `userStorage.canWrite$()`, `userProfile` dependency dropped entirely.
- `navigation_customization_service.ts` — the only code broken by the async `get()` reshape; its two calls switched to `peek()` (its key is `preload:true`, so behavior is unchanged).
@tsullivan
tsullivan force-pushed the user-storage/new-browser-api branch from df64072 to 83a494e Compare July 24, 2026 20:45
@tsullivan
tsullivan marked this pull request as ready for review July 24, 2026 20:45
@tsullivan
tsullivan requested review from a team as code owners July 24, 2026 20:45
@tsullivan tsullivan added release_note:skip Skip the PR/issue when compiling release notes backport:skip This PR does not require backporting labels Jul 24, 2026

@davismcphee davismcphee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code-only review, Data Discovery changes LGTM 👍 It wasn't clear to me if there was something I needed to manually test here.

tsullivan and others added 2 commits July 24, 2026 17:50
Co-authored-by: Davis McPhee <davismcphee@hotmail.com>
@kibanamachine

Copy link
Copy Markdown
Contributor

💛 Build succeeded, but was flaky

Failed CI Steps

Test Failures

  • [job] [logs] FTR Configs #25 / discover/context_awareness extension getPaginationConfig "before all" hook in "extension getPaginationConfig"
  • [job] [logs] Scout Lane #17 - stateful-classic / default / local-stateful-classic - Date range picker presets persistence - persists save and delete across reloads

Metrics [docs]

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
core 501.0KB 501.8KB +825.0B
data 455.0KB 455.0KB -23.0B
navigation 18.4KB 18.4KB +2.0B
total +804.0B

History

cc @tsullivan

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:skip This PR does not require backporting release_note:skip Skip the PR/issue when compiling release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[User Storage] Consumers should not inherit complexity (service availability and lazy-cache checks)

3 participants