array() ); } function abtestkit_connected_registration_save( array $state ) : bool { update_option( ABTESTKIT_CONNECTED_REGISTRATION_OPTION, $state, false ); return get_option( ABTESTKIT_CONNECTED_REGISTRATION_OPTION, null ) === $state; } function abtestkit_connected_registration_clear() : void { delete_option( ABTESTKIT_CONNECTED_REGISTRATION_OPTION ); } /** Derive only the desired hosted state from a committed local registry row. */ function abtestkit_connected_registration_desired_state( ?array $test ) : string { if ( null === $test ) { return 'deleted'; } $status = (string) ( $test['status'] ?? '' ); if ( 'running' === $status ) { return 'active'; } return in_array( $status, array( 'winner', 'complete' ), true ) ? 'complete' : 'paused'; } /** Update the desired-state ledger only after a verified experiment write. */ function abtestkit_connected_registration_observe_change( array $before, array $after ) : void { if ( ! abtestkit_connected_platform_can_create_or_launch() ) { return; } $old = array(); $new = array(); foreach ( $before as $test ) { if ( is_array( $test ) && ! empty( $test['id'] ) ) { $old[ (string) $test['id'] ] = $test; } } foreach ( $after as $test ) { if ( is_array( $test ) && ! empty( $test['id'] ) ) { $new[ (string) $test['id'] ] = $test; } } $ledger = abtestkit_connected_registration_state(); $changed = false; foreach ( $old + $new as $test_id => $unused ) { $previous = $old[ $test_id ] ?? null; $current = $new[ $test_id ] ?? null; if ( is_array( $previous ) && is_array( $current ) && $previous === $current ) { continue; } $desired = abtestkit_connected_registration_desired_state( $current ); $key = hash( 'sha256', (string) $test_id ); $record = $ledger['records'][ $key ] ?? null; if ( is_array( $record ) && $desired === ( $record['state'] ?? '' ) ) { continue; } if ( count( $ledger['records'] ) >= 5000 && ! is_array( $record ) ) { abtestkit_connected_collection_gap( 'registration_capacity' ); continue; } $ref = abtestkit_connected_test_ref_record( (string) $test_id ); if ( is_wp_error( $ref ) ) { abtestkit_connected_collection_gap( $ref->get_error_code() ); continue; } $ledger['records'][ $key ] = array( 'test_ref' => (string) $ref['test_ref'], 'state' => $desired, 'revision' => is_array( $record ) ? min( 2147483647, (int) $record['revision'] + 1 ) : 1, 'confirmed_revision' => is_array( $record ) ? (int) ( $record['confirmed_revision'] ?? 0 ) : 0, 'confirmed_site_ref' => is_array( $record ) ? (string) ( $record['confirmed_site_ref'] ?? '' ) : '', ); $changed = true; } if ( $changed && ! abtestkit_connected_registration_save( $ledger ) ) { abtestkit_connected_collection_gap( 'registration_write_failed' ); } } /** Repair missed observations and backfill at most 20 records per cron tick. */ function abtestkit_connected_registration_reconcile( array $ledger ) : array { $tests = get_option( 'abtestkit_page_tests', array() ); if ( ! is_array( $tests ) ) { return $ledger; } $changed = 0; $seen = array(); foreach ( $tests as $test ) { if ( ! is_array( $test ) || empty( $test['id'] ) ) { continue; } $test_id = (string) $test['id']; $key = hash( 'sha256', $test_id ); $seen[ $key ] = true; if ( isset( $ledger['records'][ $key ] ) ) { $desired = abtestkit_connected_registration_desired_state( $test ); if ( $desired !== ( $ledger['records'][ $key ]['state'] ?? '' ) && $changed < 20 ) { $ledger['records'][ $key ]['state'] = $desired; $ledger['records'][ $key ]['revision'] = min( 2147483647, (int) $ledger['records'][ $key ]['revision'] + 1 ); ++$changed; } continue; } if ( $changed >= 20 || count( $ledger['records'] ) >= 5000 ) { continue; } $ref = abtestkit_connected_test_ref_record( $test_id ); if ( is_wp_error( $ref ) ) { abtestkit_connected_collection_gap( $ref->get_error_code() ); continue; } $ledger['records'][ $key ] = array( 'test_ref' => (string) $ref['test_ref'], 'state' => abtestkit_connected_registration_desired_state( $test ), 'revision' => 1, 'confirmed_revision' => 0, 'confirmed_site_ref' => '', ); ++$changed; } foreach ( $ledger['records'] as $key => $record ) { if ( $changed >= 20 ) { break; } if ( ! isset( $seen[ $key ] ) && is_array( $record ) && 'deleted' !== ( $record['state'] ?? '' ) ) { $ledger['records'][ $key ]['state'] = 'deleted'; $ledger['records'][ $key ]['revision'] = min( 2147483647, (int) $record['revision'] + 1 ); ++$changed; } } if ( $changed && ! abtestkit_connected_registration_save( $ledger ) ) { abtestkit_connected_collection_gap( 'registration_write_failed' ); } return $ledger; } /** Cron-only, at most 20 authenticated registration writes per tick. */ function abtestkit_connected_registration_run() : void { if ( ! abtestkit_connected_platform_can_create_or_launch() ) { return; } $connection = abtestkit_hosted_get_connection(); if ( ! is_array( $connection ) ) { return; } $ledger = abtestkit_connected_registration_reconcile( abtestkit_connected_registration_state() ); $sent = 0; foreach ( $ledger['records'] as $key => $record ) { if ( $sent >= 20 ) { break; } if ( ! is_array( $record ) || ( (int) ( $record['confirmed_revision'] ?? 0 ) === (int) ( $record['revision'] ?? 0 ) && (string) ( $record['confirmed_site_ref'] ?? '' ) === (string) $connection['site_ref'] ) ) { continue; } if ( ! is_string( $record['test_ref'] ?? null ) || 1 !== preg_match( '/^test_[A-Za-z0-9_-]{43}$/D', $record['test_ref'] ) || ! is_int( $record['revision'] ?? null ) || $record['revision'] < 1 || $record['revision'] > 2147483647 || ! in_array( $record['state'] ?? null, array( 'active', 'paused', 'complete', 'deleted' ), true ) ) { abtestkit_connected_registration_error( 'invalid_local_registration' ); break; } ++$sent; $response = abtestkit_hosted_service_request( 'PUT', '/sites/' . (string) $connection['site_ref'] . '/experiments/' . (string) $record['test_ref'] . '/registration', array( 'schema_version' => 1, 'revision' => (int) $record['revision'], 'state' => (string) $record['state'] ), (string) $connection['credential'] ); if ( is_wp_error( $response ) ) { abtestkit_connected_registration_error( 'transport' ); break; } $status = (int) $response['status']; if ( 401 === $status ) { if ( ! abtestkit_connected_registration_revoke_connection( $connection ) ) { abtestkit_connected_registration_error( 'credential_changed' ); } break; } if ( 404 === $status ) { // An older service may not have this route; 404 is not proof of // revocation. Keep local execution and retry after deployment. abtestkit_connected_registration_error( 'service_route_unavailable' ); break; } if ( 403 === $status && in_array( $response['data']['code'] ?? '', array( 'abtk_hosted_platform_agreement_required', 'abtk_hosted_account_inactive' ), true ) ) { if ( ! abtestkit_connected_registration_revoke_connection( $connection ) ) { abtestkit_connected_registration_error( 'credential_changed' ); } break; } $data = $response['data']; if ( ! in_array( $status, array( 200, 201 ), true ) || ! is_array( $data ) || (int) ( $data['schema_version'] ?? 0 ) !== 1 || (int) ( $data['revision'] ?? 0 ) !== (int) $record['revision'] || (string) ( $data['state'] ?? '' ) !== (string) $record['state'] || ! is_bool( $data['duplicate'] ?? null ) ) { abtestkit_connected_registration_error( $status >= 500 || 429 === $status ? 'service_unavailable' : 'service_rejected' ); break; } $ledger['records'][ $key ]['confirmed_revision'] = (int) $record['revision']; $ledger['records'][ $key ]['confirmed_site_ref'] = (string) $connection['site_ref']; if ( ! abtestkit_connected_registration_save( $ledger ) ) { abtestkit_connected_registration_error( 'storage_unavailable' ); break; } abtestkit_connected_registration_error( '' ); $ledger = abtestkit_connected_registration_state(); } } add_action( ABTESTKIT_CONNECTED_TRANSPORT_HOOK, 'abtestkit_connected_registration_run', 20 ); /** A confirmed credential/agreement denial removes only the connection used by the request. */ function abtestkit_connected_registration_revoke_connection( array $connection ) : bool { return abtestkit_hosted_remove_local_connection( $connection ); } /** Local diagnostic only; no payloads or credentials. */ function abtestkit_connected_registration_error( string $code ) : void { $ledger = abtestkit_connected_registration_state(); $ledger['last_error_code'] = $code; $ledger['last_attempt_at'] = time(); if ( '' === $code ) { $ledger['last_success_at'] = time(); } abtestkit_connected_registration_save( $ledger ); }