Skip to content

feat(session-sync): phased SW resync with experiment-ready config#573

Open
Just-Insane wants to merge 4 commits intoSableClient:devfrom
Just-Insane:feat/sw-session-resync-flags
Open

feat(session-sync): phased SW resync with experiment-ready config#573
Just-Insane wants to merge 4 commits intoSableClient:devfrom
Just-Insane:feat/sw-session-resync-flags

Conversation

@Just-Insane
Copy link
Copy Markdown
Contributor

@Just-Insane Just-Insane commented Mar 28, 2026

📝 Docs PR: SableClient/docs#9 — merge that when merging this.


PR dependency (stacked)

This PR currently includes shared infra/plumbing commits that are introduced in #572.
Please review/merge #572 first, then review this PR for session-sync behavior.

Description

Adds phased service-worker session re-sync controls, gated behind an experiment flag with direct-config fallback.

Fixes #

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings

AI disclosure:

  • Partially AI assisted (clarify which code was AI assisted and briefly explain what it does).
  • Fully AI generated (explain what all the generated code does in moderate detail).
    The phased session-sync hooks/flags and config integration were partially AI assisted; I verified branch behavior and safe defaults.

Feature Flags / Environment Setup

Set these GitHub Environment variables:

  • CLIENT_CONFIG_OVERRIDES_JSON
  • CLIENT_CONFIG_OVERRIDES_STRICT (optional)

How it works

Phase flags are derived in useAppVisibility from the sessionSyncStrategy experiment:

Variant Phase 1 (foreground resync) Phase 2 (heartbeat) Phase 3 (adaptive backoff)
control (default)
session-sync-heartbeat
session-sync-adaptive

Users not in the experiment fall back to the sessionSync.phase* booleans, which can be used for direct-config overrides (e.g. in preview/staging where you want all users to get the feature without bucketing).

sessionSync config field reference

Field Type Default Description
phase1ForegroundResync boolean false Push session credentials to the SW on page focus / becoming visible.
phase2VisibleHeartbeat boolean false Periodic heartbeat that keeps SW credentials fresh while the tab is visible and online.
phase3AdaptiveBackoffJitter boolean false Exponential backoff with ±20% jitter on heartbeat failures. No effect unless phase2VisibleHeartbeat is true.
foregroundDebounceMs number 1500 Minimum ms between foreground-triggered pushes.
heartbeatIntervalMs number 600000 Base interval between heartbeat ticks (ms).
resumeHeartbeatSuppressMs number 60000 How long after a foreground push to suppress the next heartbeat tick (ms).
heartbeatMaxBackoffMs number 1800000 Maximum backoff ceiling for adaptive mode (ms).

Recommended initial values

preview — enable for all users directly (no bucketing):

{
  "sessionSync": {
    "phase1ForegroundResync": true,
    "phase2VisibleHeartbeat": true,
    "phase3AdaptiveBackoffJitter": true
  }
}

production — gradual rollout via experiment:

Note: rolloutPercentage controls what fraction of users enter the experiment at all. Those users are then split evenly across variants. So rolloutPercentage: 20 with 2 variants = 10% per variant, 80% control.

{
  "experiments": {
    "sessionSyncStrategy": {
      "enabled": true,
      "rolloutPercentage": 20,
      "controlVariant": "control",
      "variants": ["session-sync-heartbeat", "session-sync-adaptive"]
    }
  }
}

No environment variable setup is required for deployment to succeed; missing overrides are treated as no-op and checked-in defaults are used.

@Just-Insane Just-Insane requested review from 7w1 and hazre as code owners March 28, 2026 18:33
Copilot AI review requested due to automatic review settings March 28, 2026 18:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds session re-sync phase controls (foreground + visible heartbeat with optional adaptive backoff/jitter) and introduces an experiment/config override pipeline so behavior can be rolled out via environment-scoped config.json overrides at build time.

Changes:

  • Extend client config schema with sessionSync toggles and experiments, plus helper APIs to deterministically bucket users into variants.
  • Update app visibility handling to optionally push the active session to the service worker on foreground/focus and on a visible heartbeat loop.
  • Add a CI/build-time config.json override injector and wire it into GitHub Actions (preview/production environments), plus update default config.json.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/app/pages/client/ClientRoot.tsx Passes activeSession into useAppVisibility so SW session sync can include full session identity.
src/app/hooks/useClientConfig.ts Adds sessionSync/experiments config shape and experiment variant selection helpers.
src/app/hooks/useAppVisibility.ts Implements phased SW session pushes (foreground/focus + heartbeat + optional adaptive backoff/jitter).
scripts/inject-client-config.js New build-time script to deep-merge env overrides into config.json.
knip.json Adds the new script as a Knip entry.
config.json Adds default experiments and sessionSync config blocks.
.github/workflows/cloudflare-web-preview.yml Exposes env-scoped config override vars to preview deploy workflow.
.github/workflows/cloudflare-web-deploy.yml Exposes env-scoped config override vars to plan/apply workflows (preview + production).
.github/actions/setup/action.yml Runs the config override injector as part of build setup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Just-Insane
Copy link
Copy Markdown
Contributor Author

Addressing the three Copilot review comments from this PR:

scripts/inject-client-config.js — prototype pollution in deepMerge

Added an UNSAFE_KEYS guard that skips __proto__, constructor, and prototype during the merge loop. These keys are data-only in any legitimate config object, so skipping them is safe with no functional impact on real overrides.

scripts/inject-client-config.jslogger.error/logger.warn dropping error details

Added a formatError helper (error.stack ?? error.message for Error objects, String(error) otherwise). All logger.error(msg, err) calls are now logger.error(\${msg} ${formatError(error)}`). The invalid logger.warn(msg, err)call (which would have thrownTypeError: logger.warn is not a function) is replaced with logger.info(`[warning] ${msg} ${formatError(error)}`)`.

src/app/hooks/useAppVisibility.ts — two heartbeat backoff issues

  1. Prerequisite-not-ready skips growing backoff: Changed pushSessionNow return type from boolean to 'sent' | 'skipped'. The tick loop now only resets heartbeatFailuresRef when the result is 'sent' — skipped calls (no mx, no SW controller, etc.) are not treated as push failures and won't ramp up backoff during startup.

  2. Stale backoff/suppression state carrying over across effect restarts: Added heartbeatFailuresRef.current = 0 and suppressHeartbeatUntilRef.current = 0 at the top of the heartbeat useEffect body. Config or session changes that cause the effect to re-run now start with a clean slate.

@Just-Insane Just-Insane marked this pull request as draft March 29, 2026 13:13
@Just-Insane Just-Insane marked this pull request as ready for review March 29, 2026 17:52
@Just-Insane Just-Insane force-pushed the feat/sw-session-resync-flags branch from 0cd0e61 to a1445fe Compare March 29, 2026 18:05
Shared build infrastructure: inject-client-config.js reads config keys
from GH Actions env vars and merges them into config.json. CI workflows
pass these through; setup action prints an injected-config summary.
Required by the sessionSyncStrategy experiment below.
…ntages

getExperimentVariant() deterministically buckets userId into a variant
using a hash, checked against rolloutPercentage. Named sessionSyncStrategy
experiment wired to phase flags that control SW resync behaviour.
Three opt-in phases controlled by sessionSync flags in client config:
  - phase1ForegroundResync: resync session on app foreground
  - phase2VisibleHeartbeat: periodic heartbeat while app is visible
  - phase3AdaptiveBackoffJitter: exponential backoff with jitter

Phase flags are automatically set from the sessionSyncStrategy experiment
variant so rollout can be controlled via injected client config. Wired
into ClientRoot via useAppVisibility.
@Just-Insane Just-Insane force-pushed the feat/sw-session-resync-flags branch from a1445fe to fa6d468 Compare March 29, 2026 18:17
Just-Insane added a commit to Just-Insane/docs that referenced this pull request Mar 29, 2026
Add sessionSync config block documentation to the installation
guide, including all timing fields, the three phase flags, and
how to combine with experiments.sessionSyncStrategy for a
controlled rollout.

Corresponds to SableClient/Sable#573
Just-Insane added a commit to Just-Insane/docs that referenced this pull request Mar 29, 2026
Add sessionSync config block documentation to the installation
guide, including all timing fields, the three phase flags, and
how to combine with experiments.sessionSyncStrategy for a
controlled rollout.

Corresponds to SableClient/Sable#573
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants