Skip to content

feat: add registerPlugin so a plugin can be added after the client starts - #522

Merged
abelonogov-ld merged 3 commits into
v11from
andrey/register-plugin
Aug 25, 2026
Merged

feat: add registerPlugin so a plugin can be added after the client starts#522
abelonogov-ld merged 3 commits into
v11from
andrey/register-plugin

Conversation

@abelonogov-ld

@abelonogov-ld abelonogov-ld commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Plugins could only be supplied through LDConfig, so an integration that learns about a plugin later — or that wants to instrument a client it did not configure — had no way in. This adds LDClient.registerPlugin(_:), matching the method the .NET and Flutter SDKs already expose and the one being added to the Android SDK in launchdarkly/android-client-sdk#393.

  • Hooks became mutable, because registration after start cannot extend a constant array. They now live in a private var behind an NSLock and are replaced rather than mutated in place. Each series reads a snapshot once into a local, so the hooks a series ends with are the hooks it began with — were the array read again mid-series, a hook registered in between would be handed an after stage for a series whose before stage it was never in. The three read sites (evaluateWithHooks, executeBeforeIdentifyHooks, executeAfterTrackHooks) all take that snapshot up front.
  • Hooks go live only once register returns. This matches the Android and .NET ordering, and differs from configuration-time registration where a plugin's hooks are active before register is called. So a plugin's own hooks will not observe evaluations or identify calls its register makes; everything afterwards does run them.
  • EnvironmentMetadata is retained on the instance, so a plugin registered later is handed the same environment description as one configured up front. This also removes the duplicate construction that previously existed in both start and collectHooks.

Registration applies to the one client it is called on, so a multi-environment setup means calling it per environment.

Note that the iOS Plugin protocol has no onPluginsReady, so unlike Android this path is just getHooks then register then activate.

Test plan

Five cases added to LDClientPluginsSpec, all passing on an iPhone 16e simulator alongside the two pre-existing plugin tests (7 total):

  • testRegisterPluginPassesClientAndEnvironmentMetadata
  • testRegisterPluginActivatesBundledHooks
  • testRegisterPluginDoesNotRunTheRegisteringPluginsOwnHooks
  • testRegisterPluginHooksRunAfterConfiguredHooks
  • testRegisterPluginAppliesOnlyToTheClientItIsCalledOn
  • SwiftLint introduces no new violations in the touched files

Note

Overview
Adds LDClient.registerPlugin(_:) so integrations can attach plugins after start, aligned with other LaunchDarkly mobile SDKs. Plugins still use LDConfig.plugins at startup; this path is for late discovery or clients the app did not configure.

Hook list is now mutable and thread-safe. Hooks move from a fixed let array to storedHooks guarded by NSLock, with addHooks appending plugin hooks at registration. Evaluation, identify, and track paths each snapshot hooks once per series so mid-flight registerPlugin cannot pair an after stage with a before the new hook never ran.

environmentMetadata is stored on the client and passed to register and getHooks, so late plugins get the same environment description as config-time plugins and duplicate metadata construction in start / collectHooks is removed.

registerPlugin calls getHooks and activates hooks before Plugin.register, matching config-time ordering (including work done inside register). Registration applies only to the client instance you call it on (multi-environment setups need per-client calls).

Tests in LDClientPluginsSpec cover metadata handoff, hook activation, ordering after config hooks, hooks during register, and per-environment isolation.

Reviewed by Cursor Bugbot for commit 1c5db77. Bugbot is set up for automated code reviews on this repo. Configure here.

…arts

Plugins could only be supplied through LDConfig, so an integration that
learns about a plugin later — or that wants to instrument a client it did
not configure — had no way in.

Hooks were held in a constant array, which registration after start cannot
extend, so they now live behind a lock and are replaced rather than mutated
in place. Each series reads a snapshot once, so the hooks a series ends with
are the hooks it began with: read again mid-series, a hook registered in
between would be handed an "after" stage for a series whose "before" stage
it was never in.

Hooks go live only once register returns, matching the Android and .NET
ordering, so a plugin's own hooks do not observe its register call.
Retaining EnvironmentMetadata on the instance lets a plugin registered
later be handed the same environment description as one configured up
front, and removes the duplicate construction in start and collectHooks.

Co-authored-by: Cursor <cursoragent@cursor.com>
@abelonogov-ld
abelonogov-ld requested a review from a team as a code owner August 21, 2026 20:31
guard !newHooks.isEmpty else { return }

hooksLock.lock()
defer { hooksLock.unlock() }

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.

defer is not always without cost. Do you need defer here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LDClient is the class and defer would be negligible compare to lock/unlock itself

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

defer is considered good pattern for locking

/// be given an "after" stage for a series whose "before" stage it was never in.
var hooks: [Hook] {
hooksLock.lock()
defer { hooksLock.unlock() }

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.

defer is not always without cost. Do you need defer here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LDClient is the class and defer would be negligible compare to lock/unlock itself

public func registerPlugin(_ plugin: Plugin) {
let pluginHooks = plugin.getHooks(metadata: environmentMetadata)
plugin.register(client: self, metadata: environmentMetadata)
addHooks(pluginHooks)

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.

I think addHooks should be before register.

While hooks could be added during register, if the SDK supports runtime hook registration, this could result in a plugin missing hook invocations. This is because other plugins could call track, identify, or variation methods during registration. Getting a list of hooks before
  registration ensures that the plugin cannot miss any operations.

@abelonogov-ld
abelonogov-ld merged commit 229ef24 into v11 Aug 25, 2026
18 checks passed
@abelonogov-ld
abelonogov-ld deleted the andrey/register-plugin branch August 25, 2026 16:09
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