Skip to content

Feat/dev 4038 multiinstance - #210

Merged
Xyzcancer merged 7 commits into
masterfrom
feat/DEV-4038-multiinstance
Aug 13, 2026
Merged

Feat/dev 4038 multiinstance#210
Xyzcancer merged 7 commits into
masterfrom
feat/DEV-4038-multiinstance

Conversation

@Xyzcancer

Copy link
Copy Markdown
Contributor

No description provided.

Release 2 of multi-instance, storage layer. The default preferences file was a
single shared constant, so instances for different shops collided on did/sid.
initialize() now derives a per-shop partition name (PreferencesPartition) when
the host leaves preferencesKey at its default; a host that passes its own key
keeps using it verbatim, with no derivation.

To stay transparent, the first init of a shop's partition runs a one-time
migration out of the old shared file: it copies every entry only while the
partition is still empty and only when the legacy file belongs to that shop
(its stored shop_id matches, or is absent for a very old install). An existing
single-instance install keeps its identity after upgrading; a partition for a
different shop stays clean.

No public API change (preferencesKey keeps its default and meaning) and no
behaviour change for a single-instance host beyond the file it reads. Covered
by PreferencesPartitionTest and PreferencesDataSourceMigrationTest (11 tests,
migration guards mutation-checked). Not wired into the demo.
…registerShops

Release 3 of multi-instance: open the unified public entry point. A host no
longer keeps its own SDK reference — it initializes (or registers) shops
through Rees46 and reaches them by shopId.

- Rees46.initialize(context, Rees46Config) creates and registers an instance.
- Rees46.registerShops(context, configs, eagerInit=false) records shops and
  initializes them lazily on first getInstance (the region case), or up front
  when eagerInit is true (the super-shop case).
- Rees46.getInstance(shopId?) returns the instance; with no shopId it returns
  the single one, throwing AmbiguousSdkInstanceException when several shops are
  registered and SdkNotInitializedException when nothing matches.

Instance state was already isolated: initialize() builds a fresh Dagger graph
per instance and storage is partitioned per shop (Release 2), so this layer is
routing on top of SdkRegistry plus a pending-config map for lazy init. The
resolution rules live in a side-effect-free InstanceResolver so every branch is
unit-tested without constructing an SDK.

SDK.initialize is now @deprecated with a ReplaceWith to Rees46.initialize; it
still works and registers the same way. No demo changes. 27 new tests
(InstanceResolverTest, Rees46Test), resolver mutation-checked.
Adds an event-driven way to resolve an instance once it exists, instead of the
host holding a reference or the UI polling with retries.

- SdkRegistry.onNextRegister(shopId?) subscribes to the next matching register
  and returns a Cancellable; register() wakes matching awaiters.
- Rees46.awaitInstance(shopId?) { } delivers the instance immediately when it is
  already live (or materializes a pending registration), otherwise once it is
  registered; throws AmbiguousSdkInstanceException for a null shopId with several
  live shops. Rees46.isInitialized(shopId?) is the synchronous check.

- StoriesWidget gains an overload that takes shopId (default = the single
  instance) and resolves its SDK through awaitInstance, rendering nothing until
  ready and cancelling on dispose. The old StoriesWidget(sdk, ...) is @deprecated
  but still works — the demo compiles unchanged (deprecation warning only).

The deeper cleanup — StoriesView owning its load so initializeStoriesView and
StoriesManager's single held view reference go away — is a follow-up. 12 notifier
tests, fire path mutation-checked. No demo changes.
…ld view

StoriesManager held a single view reference and pushed loaded stories into it,
so two blocks on one screen fought over that reference — only the last one
registered received data (the empty-pane bug). The view now owns its load.

- StoriesManager is stateless: loadStories(code) { } fetches and parses, no view
  held. The only retained reference is a WeakReference to the last attached view,
  used solely as the surface for the view-less SDK.showStories(code) — not for
  loading.
- StoriesView self-loads on attach: it resolves its instance via
  Rees46.awaitInstance(shopId) (new app:shop_id attribute, default = the single
  instance) and loads its own block, cancelling the subscription on detach.
- initializeStoriesView keeps working (now @deprecated): it calls
  StoriesView.attach, which wires an explicit SDK and reloads, superseding any
  pending self-load so the two never double-load. The Compose wrapper uses the
  same attach path.

Verified on device: the Legacy and Compose panes now each load independently
(two /stories requests, the Legacy pane populated without the reload workaround),
and tapping a story still opens the full-screen viewer. 4 StoriesManager load
tests. No demo changes.
Release 4 of multi-instance. Rees46.handlePush(payload, event) resolves the
target instance from the payload's shop_id and dispatches a RECEIVED (track
received + message listener) or CLICKED (track clicked + click action) event.
The rule lives in a side-effect-free PushTargetResolver: shop_id names the
instance; with no shop_id a single-instance app still resolves; an unknown shop
or an absent shop_id with several live shops drops the push instead of
delivering it to the wrong one.

The SDK's own messaging entry (SDK.onMessage) now resolves by shop_id too,
keeping the current-instance fallback so single-instance apps and cold-start
delivery are unchanged. Push-token registration already fans out to every
instance, which is the super-shop token-sync behaviour.

10 tests (PushTargetResolver mutation-checked; handlePush routing verified via
the message listener on bare instances). No demo changes.
…hem by hand

Google Play reports the SDK under "Improve your app's performance with bitmap
image optimisation", pointing at NotificationImageHelper.loadBitmaps: it fetched
every push image with URL.openStream() and decoded it with BitmapFactory, so the
bitmap was allocated at the source resolution — a 4000x3000 product photo costs
~46 MB of heap for a block that is 150dp tall, and that bitmap then travels to
the system UI through a Binder transaction.

Glide is already a dependency (stories, in-app), so nothing new is pulled in. It
decodes straight into the box the notification actually shows (screen width by
150dp, capped at 1080px), keeps the decode in software since RemoteViews cannot
take a hardware bitmap, and caches the download — the notification is rebuilt on
every arrow tap, which used to re-fetch all the images. The load now times out
after 15s instead of holding NotificationService open on a stalled CDN, and the
result is copied out before the request is cleared so the pooled bitmap cannot be
handed to the next decode while the posted notification still draws it.

loadBitmaps takes a Context now; both call sites are in the SDK. Empty entries in
the comma-separated list are dropped instead of requested, and hasError is
derived from the results instead of written by racing coroutines.

The demo app posted its BigPicture notification the same manual way, so it gets
the same treatment — it is what integrators copy.
SDK:
- OnShopMessageListener + process-global shop-aware message routing
- Rees46 / SdkRegistry / InstanceResolver refinements
- per-shop preferences partition and session isolation (UserSettings, RegisterManager)
- shop_id push routing in HmsMessagingService / MessagingService

Demo:
- two-shop MultiInstancePane + DemoTheme, lazy shop registration
- device-specific legibility fixes (MIUI/Huawei) and stories layout

Tests:
- instance resolver / registry / push routing / robustness
- upgrade migration, did policy, segment A/B
@Xyzcancer
Xyzcancer merged commit 24a7f53 into master Aug 13, 2026
1 check passed
@Xyzcancer
Xyzcancer deleted the feat/DEV-4038-multiinstance branch August 13, 2026 06:32
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.

1 participant